<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>뒤태지존의 끄적거림</title>
  
  <subtitle>개발</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="http://homoefficio.github.io/"/>
  <updated>2022-08-28T03:17:32.965Z</updated>
  <id>http://homoefficio.github.io/</id>
  
  <author>
    <name>HomoEfficio</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Kafka Poison Pill Spring ErrorHandlingDeserializer</title>
    <link href="http://homoefficio.github.io/2022/08/28/Kafka-Poison-Pill-Spring-ErrorHandlingDeserializer/"/>
    <id>http://homoefficio.github.io/2022/08/28/Kafka-Poison-Pill-Spring-ErrorHandlingDeserializer/</id>
    <published>2022-08-27T16:19:22.000Z</published>
    <updated>2022-08-28T03:17:32.965Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Kafka-Poison-Pill-Spring-ErrorHandlingDeserializer"><a href="#Kafka-Poison-Pill-Spring-ErrorHandlingDeserializer" class="headerlink" title="Kafka Poison Pill Spring ErrorHandlingDeserializer"></a>Kafka Poison Pill Spring ErrorHandlingDeserializer</h1><p>카프카 메시지 consume은 대략 다음과 같은 절차로 진행된다.</p><ol><li>Fetch Serialized Data from Broker(직렬화된 데이터를 브로커로부터 가져와서)</li><li>Deserialize the fetched serialized data(정해진 타입으로 역직렬화하고)</li><li>Consume the deserialized data(역직렬화한 데이터를 처리하고)</li><li>Commit(브로커에게 commit 신호 전송)</li></ol><h2 id="받아온-메시지-처리에-실패하면"><a href="#받아온-메시지-처리에-실패하면" class="headerlink" title="받아온 메시지 처리에 실패하면.."></a>받아온 메시지 처리에 실패하면..</h2><p>스프링 카프카를 사용하면서 개발자가 @KafkaListener를 붙여 작성한 리스너 구현부는 3번 과정에 해당한다.<br>3번 과정에서 에러가 발생하면 정해진 횟수(기본 10회)만큼 재시도 후 끝까지 실패하면 대략 아래와 같은 에러 로그를 남기면서 그냥 4번 commit 신호를 브로커로 보낸다.  </p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line">2022-08-27 22:50:06.326 ERROR 64718 --- [ntainer#0-0-C-1] o.s.kafka.listener.DefaultErrorHandler   : Backoff FixedBackOff&#123;interval=0, currentAttempts=10, maxAttempts=9&#125; exhausted for basic-topic-01-1@2</span><br><span class="line"></span><br><span class="line">org.springframework.kafka.listener.ListenerExecutionFailedException: Listener method &apos;public void io.homo_efficio.scratchpad.spring.reactor.kafka.service.KafkaConsumer.listenerBasicTopic01(java.lang.String)&apos; threw exception; nested exception is java.lang.RuntimeException: 4th 메시지는 처리 불가!!!; nested exception is java.lang.RuntimeException: 4th 메시지는 처리 불가!!!</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.decorateException(KafkaMessageListenerContainer.java:2703) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeOnMessage(KafkaMessageListenerContainer.java:2673) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeOnMessage(KafkaMessageListenerContainer.java:2633) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeRecordListener(KafkaMessageListenerContainer.java:2560) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeWithRecords(KafkaMessageListenerContainer.java:2441) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:2319) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:1990) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeIfHaveRecords(KafkaMessageListenerContainer.java:1366) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1357) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1252) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]</span><br><span class="line">    at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]</span><br><span class="line">    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]</span><br><span class="line">    at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]</span><br><span class="line">    Suppressed: org.springframework.kafka.listener.ListenerExecutionFailedException: Restored Stack Trace</span><br><span class="line">        at org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:363) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">        at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:92) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">        at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:53) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">        at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeOnMessage(KafkaMessageListenerContainer.java:2653) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">Caused by: java.lang.RuntimeException: 4th 메시지는 처리 불가!!!</span><br><span class="line">    at io.homo_efficio.scratchpad.spring.reactor.kafka.service.KafkaConsumer.listenerBasicTopic01(KafkaConsumer.kt:13) ~[main/:na]</span><br><span class="line">    at jdk.internal.reflect.GeneratedMethodAccessor85.invoke(Unknown Source) ~[na:na]</span><br><span class="line">    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]</span><br><span class="line">    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]</span><br><span class="line">    at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:169) ~[spring-messaging-5.3.21.jar:5.3.21]</span><br><span class="line">    at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:119) ~[spring-messaging-5.3.21.jar:5.3.21]</span><br><span class="line">    at org.springframework.kafka.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:56) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:347) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:92) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:53) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeOnMessage(KafkaMessageListenerContainer.java:2653) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    ... 12 common frames omitted</span><br></pre></td></tr></table></figure><p>브로커는 commit 신호를 받아서 해당 토픽, 파티션의 offset을 증가시킨다.<br>컨수머 입장에서는 해당 데이터 처리에 실패했지만, 브로커 입장에서는 정상적으로 데이터가 처리됐을 때와 마찬가지로 commit 신호를 받으므로 장애는 발생하지 않는다.</p><h2 id="역직렬화에-실패하면"><a href="#역직렬화에-실패하면" class="headerlink" title="역직렬화에 실패하면.."></a>역직렬화에 실패하면..</h2><p>그런데 2번 역직렬화 과정에서 에러가 나면 어떻게 될까?</p><p>예를 들어 JsonSerializer/JsonDeserializer를 사용해서 JSON으로 직렬화/역직렬화하도록 구성했는데,<br>JSON 규격에 맞지 않는 메시지(poison pill)가 인입되면 역직렬화 과정에서 에러가 발생하게 된다.</p><p>2번 <strong>역직렬화 과정은 3번 과정보다 앞서 진행되므로 2번 과정에서 에러가 발생하면 개발자가 작성한 리스너로 진입하지 못하며,</strong><br>그 앞 단계에서 즉 프레임워크 수준에서 에러 처리가 돼야 한다.<br>그런데 이 에러 처리를 별로 신경쓰지 않고 사용하다가 역직렬화 과정에서 에러가 발생하면,<br>3번 과정에서 처럼 정해진 횟수만큼 재시도하다가 얌전히 포기하고 commit 처리되는 게 아니라,<br><strong>역직렬화에 성공할 때까지 계속, 그것도 아주 빠른 속도로 재시도한다.</strong></p><p>그런데 역직렬화 실패한 데이터를 다시 역직렬화 하면 성공할 수 있을까?<br>그럴 가능성은 거의 없다고 생각하는데 안타깝게도 카프카 컨수머는, 아니 최소한 <strong>카프카 스프링 컨수머는 역직렬화 안 되면 될 때까지!! 서버 빠개질때까지!! 그렇게 불사파처럼 무대뽀로 동작한다.</strong></p><p><img src="https://i.imgur.com/ly5PPnv.png" alt="Imgur"></p><p>그 결과 컨수머 시스템 자원이 갉아먹히는데,<br><strong>이 컨수머가 다른 토픽의 컨수머이기도 하면 자원 부족으로 그 다른 토픽의 메시지를 컨숨하는 속도가 느려지고 그 다른 토픽에도 메시지가 쌓이게 된다.</strong><br><strong>결국 이 컨수머를 포함하고 있는 서버 인스턴스들은 점점 좀비화되고 서비스 장애 상황으로 이어진다.</strong></p><p>그러니 역직렬화에 실패하면,<br>마치 혁명에 실패하면 반역죄로 능지처사에 3족이 멸문 당하는 것처럼 아주 걍 작살이 나는 거시다.</p><h2 id="Spring-ErrorHandlingDeserializer"><a href="#Spring-ErrorHandlingDeserializer" class="headerlink" title="Spring ErrorHandlingDeserializer"></a>Spring ErrorHandlingDeserializer</h2><p>위에 ‘(역직렬화 과정) 에러 처리를 별로 신경쓰지 않고 기본 설정으로 두고 사용하면’이라는 단서가 있는데,<br>바꿔말하면 역직렬화 에러 처리를 신경쓰면 위와 같은 장애 발생을 막을 수 있다. 어떻게?</p><p><strong>스프링 카프카 2.2에 도입된 ErrorHandlingDeserializer를 사용하고, ErrorHandler를 지정해주면 된다.</strong></p><p>이 내용은 역직렬화 재시도를 계속하면서 펑펑 싸대는 아래와 같은 로그에 나름 친절하게 나와있다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">2022-08-27 14:03:22.077 ERROR 81052 --- [ntainer#0-0-C-1] o.s.k.l.KafkaMessageListenerContainer    : Consumer exception</span><br><span class="line"></span><br><span class="line">java.lang.IllegalStateException: This error handler cannot process &apos;SerializationException&apos;s directly; please consider configuring an &apos;ErrorHandlingDeserializer&apos; in the value and/or key deserializer</span><br><span class="line">    at org.springframework.kafka.listener.DefaultErrorHandler.handleOtherException(DefaultErrorHandler.java:149) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.handleConsumerException(KafkaMessageListenerContainer.java:1799) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1298) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]</span><br><span class="line">    at java.base/java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) ~[na:na]</span><br><span class="line">    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na]</span><br><span class="line">    at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]</span><br><span class="line">Caused by: org.apache.kafka.common.errors.RecordDeserializationException: Error deserializing key/value for partition basic-topic-01-2 at offset 5. If needed, please seek past the record to continue consumption.</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher.parseRecord(Fetcher.java:1448) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher.access$3400(Fetcher.java:135) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher$CompletedFetch.fetchRecords(Fetcher.java:1671) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher$CompletedFetch.access$1900(Fetcher.java:1507) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher.fetchRecords(Fetcher.java:733) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher.fetchedRecords(Fetcher.java:684) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.KafkaConsumer.pollForFetches(KafkaConsumer.java:1277) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1238) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.apache.kafka.clients.consumer.KafkaConsumer.poll(KafkaConsumer.java:1211) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollConsumer(KafkaMessageListenerContainer.java:1522) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doPoll(KafkaMessageListenerContainer.java:1512) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.pollAndInvoke(KafkaMessageListenerContainer.java:1340) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1252) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    ... 4 common frames omitted</span><br><span class="line">Caused by: java.lang.IllegalStateException: No type information in headers and no default type provided</span><br><span class="line">    at org.springframework.util.Assert.state(Assert.java:76) ~[spring-core-5.3.21.jar:5.3.21]</span><br><span class="line">    at org.springframework.kafka.support.serializer.JsonDeserializer.deserialize(JsonDeserializer.java:583) ~[spring-kafka-2.8.7.jar:2.8.7]</span><br><span class="line">    at org.apache.kafka.clients.consumer.internals.Fetcher.parseRecord(Fetcher.java:1439) ~[kafka-clients-3.1.1.jar:na]</span><br><span class="line">    ... 16 common frames omitted</span><br></pre></td></tr></table></figure><p>물론 공식 문서에도 나와있다.<br>(하지만 우리는 에러가 발생하기 전까지는 공식 문서를 잘 안..=3=3)</p><p>어쨌든 ErrorHandlingDeserializer 설정 방법이나 알아보자.</p><h3 id="ErrorHandlingDeserializer"><a href="#ErrorHandlingDeserializer" class="headerlink" title="ErrorHandlingDeserializer"></a>ErrorHandlingDeserializer</h3><p>스프링 카프카 설정 방법은 여러가지인데 결국에는 KafkaConsumerFactory에 ErrorHandlingDeserializer를 지정해주면 된다.</p><p>소스 코드로는 대략 다음과 같이 작성하면 되고,</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// from https://docs.spring.io/spring-kafka/reference/html/#error-handling-deserializer</span></span><br><span class="line"></span><br><span class="line">... <span class="comment">// other props</span></span><br><span class="line">props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.<span class="keyword">class</span>);</span><br><span class="line">props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.<span class="keyword">class</span>);</span><br><span class="line"></span><br><span class="line">props.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, JsonDeserializer.<span class="keyword">class</span>);</span><br><span class="line">props.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.<span class="keyword">class</span>.getName());</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> DefaultKafkaConsumerFactory&lt;&gt;(props);</span><br></pre></td></tr></table></figure><p>yml로 다음과 같이 작성해도 효과는 같다.</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span>  </span><br><span class="line"><span class="attr">  kafka:</span></span><br><span class="line"><span class="attr">    consumer:</span></span><br><span class="line"><span class="attr">      bootstrap-servers:</span></span><br><span class="line"><span class="attr">        - localhost:</span><span class="number">29092</span></span><br><span class="line"><span class="attr">      key-deserializer:</span> <span class="string">org.springframework.kafka.support.serializer.ErrorHandlingDeserializer</span></span><br><span class="line"><span class="attr">      value-deserializer:</span> <span class="string">org.springframework.kafka.support.serializer.ErrorHandlingDeserializer</span></span><br><span class="line"><span class="attr">      properties:</span></span><br><span class="line"><span class="attr">        spring:</span></span><br><span class="line"><span class="attr">          deserializer:</span></span><br><span class="line"><span class="attr">            key:</span></span><br><span class="line"><span class="attr">              delegate:</span></span><br><span class="line"><span class="attr">                class:</span> <span class="string">org.springframework.kafka.support.serializer.JsonDeserializer</span></span><br><span class="line"><span class="attr">            value:</span></span><br><span class="line"><span class="attr">              delegate:</span></span><br><span class="line"><span class="attr">                class:</span> <span class="string">org.springframework.kafka.support.serializer.JsonDeserializer</span></span><br></pre></td></tr></table></figure><p>결국 <strong>ErrorHandlingDeserializer로 JsonDeserializer를 감싸서, 에러가 있으면 ErrorHandler에게 넘기고, 에러가 없으면 JsonDeserializer에게 넘기는 구조다.</strong></p><h3 id="ErrorHandler"><a href="#ErrorHandler" class="headerlink" title="ErrorHandler"></a>ErrorHandler</h3><p>에러 핸들러 지정 방식도 여러가지인데 가장 간단하게는 KafkaListenerContainerFactory에 ErrorHandler를 지정해주면 된다.</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">kafkaListenerContainerFactory</span><span class="params">()</span></span>: ConcurrentKafkaListenerContainerFactory&lt;String, String&gt; &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">val</span> factory = ConcurrentKafkaListenerContainerFactory&lt;String, String&gt;()</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ErrorHandler 지정</span></span><br><span class="line">    factory.setCommonErrorHandler(                  </span><br><span class="line">        DefaultErrorHandler &#123; record, exception -&gt;</span><br><span class="line">            log.error(</span><br><span class="line">                <span class="string">"""</span></span><br><span class="line"><span class="string">                    Consume 실패!!!</span></span><br><span class="line"><span class="string">                    cause: <span class="subst">$&#123;exception.message&#125;</span></span></span><br><span class="line"><span class="string">                    topic: <span class="subst">$&#123;record.topic()&#125;</span></span></span><br><span class="line"><span class="string">                    partition: <span class="subst">$&#123;record.partition()&#125;</span></span></span><br><span class="line"><span class="string">                    offset: <span class="subst">$&#123;record.offset()&#125;</span></span></span><br><span class="line"><span class="string">                    message key: <span class="subst">$&#123;record.key()&#125;</span></span></span><br><span class="line"><span class="string">                    message value: <span class="subst">$&#123;record.value()&#125;</span></span></span><br><span class="line"><span class="string">                    <span class="subst">$&#123;exception.stackTraceToString()&#125;</span></span></span><br><span class="line"><span class="string">                """</span>.trimIndent()</span><br><span class="line">            )</span><br><span class="line">        &#125;</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    factory.consumerFactory = consumerFactory()  <span class="comment">// 앞서 ErrorHandlingDeserializer 설정할 때 만든 DefaultKafkaConsumerFactory</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> factory</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>단순히 로깅만으로 끝내도 되면 위와 같이 간단히 처리할 수 있고, 그게 아니라면 dead-letter topic으로 전송되게 할 수도 있다.</p><p>dead-letter topic 관련은 <a href="https://docs.spring.io/spring-kafka/reference/html/#dead-letters" target="_blank" rel="noopener">https://docs.spring.io/spring-kafka/reference/html/#dead-letters</a> 를 참고하자.</p><p>이밖에도 @KafkaListener에서 에러 처리를 지정할 수도 있고,<br><code>consumerProps.put(ErrorHandlingDeserializer.VALUE_FUNCTION, FailedFooProvider.class)</code> 이런 식으로 역직렬화에 실패한 메시지 대신에 다른 fallback 메시지를 만들어서 리스너에게 전달해줄 수도 있다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><blockquote><ul><li><strong>카프카 스프링 컨수머에서 역직렬화에 실패하면 완전히 새된다.</strong></li><li>StringDeserializer를 사용하면 역직렬화 실패 가능성은 매우 낮아지지만, 실제 비즈니스에서 사용하려면 역직렬화한 문자열을 다시 업무에 사용하는 데이터 타입으로 변환해줘야 한다.</li><li>이 과정을 JSON으로 한 번에 해주는 게 JsonDeserializer</li><li>하지만 JsonDeserializer를 날로 그냥 먹으면 심각한 식중독에 걸릴 수 있다.</li><li><strong>JsonDeserializer 사용 등 역직렬화 오류가 발생할 수 있는 상황에서는 반드시 ErrorHandlingDeserializer를 사용해야 Poison Pill로 인한 장애를 막을 수 있다.</strong></li></ul></blockquote><h2 id="참고"><a href="#참고" class="headerlink" title="참고"></a>참고</h2><ul><li><a href="https://docs.spring.io/spring-kafka/reference/html/#error-handling-deserializer" target="_blank" rel="noopener">https://docs.spring.io/spring-kafka/reference/html/#error-handling-deserializer</a></li><li><a href="https://www.confluent.io/ko-kr/blog/spring-kafka-can-your-kafka-consumers-handle-a-poison-pill/" target="_blank" rel="noopener">https://www.confluent.io/ko-kr/blog/spring-kafka-can-your-kafka-consumers-handle-a-poison-pill/</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Kafka-Poison-Pill-Spring-ErrorHandlingDeserializer&quot;&gt;&lt;a href=&quot;#Kafka-Poison-Pill-Spring-ErrorHandlingDeserializer&quot; class=&quot;headerlink&quot;
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Spring" scheme="http://homoefficio.github.io/tags/Spring/"/>
    
      <category term="Kafka" scheme="http://homoefficio.github.io/tags/Kafka/"/>
    
      <category term="Consumer" scheme="http://homoefficio.github.io/tags/Consumer/"/>
    
      <category term="Deserializer" scheme="http://homoefficio.github.io/tags/Deserializer/"/>
    
      <category term="Poison Pill" scheme="http://homoefficio.github.io/tags/Poison-Pill/"/>
    
      <category term="ErrorHandlingDeserializer" scheme="http://homoefficio.github.io/tags/ErrorHandlingDeserializer/"/>
    
  </entry>
  
  <entry>
    <title>Zero Downtime Migration - Design</title>
    <link href="http://homoefficio.github.io/2022/05/21/Zero-Downtime-Migration-Design/"/>
    <id>http://homoefficio.github.io/2022/05/21/Zero-Downtime-Migration-Design/</id>
    <published>2022-05-21T11:09:58.000Z</published>
    <updated>2022-08-27T16:16:44.689Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Zero-Downtime-Migration-Design"><a href="#Zero-Downtime-Migration-Design" class="headerlink" title="Zero Downtime Migration - Design"></a>Zero Downtime Migration - Design</h1><p>오징어 전문 쇼핑몰을 만들었는데 입소문이 잘 났다. 그래서 포털 몇 군데에도 API 열어줬더니 대애애애박이 났다. 물 들어 온 김에 노저어야지. 해산물 종합 쇼핑몰로 확장하려고 한다.</p><p>그런데 아뿔싸.. 엔티티 이름, 테이블 이름 등이 <code>squid</code>로 돼 있다. 잘못된 건 없다. 애초에 오징어 전문 쇼핑몰이 목표였으니까. 요구사항이 바뀌었고 그에 따라 시스템을 전환해야 하는 일이 생겼을 뿐 잘못된 것은 없다.</p><p>어떻게 전환해야 할까? 시스템을 멈추고 점검 화면을 보여주고 전환해도 될 것 같지만, 우리를 돈쭐내고 싶어 안달이난 사용자들을 오랜 시간 대기하게 만드는 것은 예의가 아니다.</p><p><img src="https://i.imgur.com/LYJSPU8.jpg" alt="Imgur"></p><p>시스템 중단 없이 마이그레이션 해보자.</p><h1 id="요구사항"><a href="#요구사항" class="headerlink" title="요구사항"></a>요구사항</h1><ul><li>마이그레이션 전/중/후에 사용자 요청 처리에 아무 오류가 없어야 한다.</li><li>API 클라이언트는 마이그레이션 후에도 당분간 기존 API를 사용할 수 있어야 각자 편한 시기에 새 API로 변경할 수 있어야 한다.</li><li><code>squid</code> 대신 <code>seafood</code> 엔티티가 중심이 되고 <code>squid</code>는 <code>seafood</code>의 여러 타입 중 하나가 된다.</li></ul><h1 id="일반적인-마이그레이션-시나리오"><a href="#일반적인-마이그레이션-시나리오" class="headerlink" title="일반적인 마이그레이션 시나리오"></a>일반적인 마이그레이션 시나리오</h1><p>편의상 기존 오징어 전문 쇼핑몰은 OLD 라고 하고, 해산물 종합 쇼핑몰은 NEW 라고 하면 대략 다음과 같은 시나리오로 전환 작업이 진행된다.</p><ol><li><code>squid</code> 대신 <code>seafood</code> 기준으로 동작하는 NEW 애플리케이션 작성</li><li><code>squid</code> 기준의 OLD db에서 <code>seafood</code> 기준으로 구성된 NEW db로의 데이터 마이그레이션 구성</li><li>NEW 애플리케이션 배포</li><li>OLD 애플리케이션으로 향하던 트래픽을 NEW 애플리케이션으로 향하도록 라우팅 변경</li></ol><h1 id="Command와-Query의-분리"><a href="#Command와-Query의-분리" class="headerlink" title="Command와 Query의 분리"></a>Command와 Query의 분리</h1><p>시스템의 상태를 변경하는 CUD 작업과 상태를 변경하지 않고 R 작업만 수행하는 기능이 하나의 애플리케이션에 들어있다면, 전환도 한 번에 할 수 밖에 없다. 한 번에 전환하는 것이 꼭 나쁘다고 할 수는 없지만, CUD와 R을 분리해서 전환하면 분리 집중, 점진적 실행 등 Divide and Conquer 전략의 일반적인 장점을 누릴 수 있다. 조금 더 구체적으로 기술하면 다음과 같다.</p><ul><li>상태 변경이 없으므로 전환 작업이 상대적으로 훨씬 수월한 R을 먼저 전환하고 모니터링하면서 <code>seafood</code> 기준의 로직에 오류가 없는지 점검하고 수정할 기회를 가질 수 있다.</li><li>R 전환에서 얻은 경험을 토대로 데이터 일관성 유지에 훨씬 더 많은 주의를 기울어야 할 CUD에 대해 더 면밀히 준비하고, 전환 이후에도 상태 변경에 대해서만 집중적으로 모니터링하고 대처할 수 있다.</li></ul><p>따라서 가능하다면 기존 애플리케이션을 CUD를 수행하는 Command와 R을 수행하는 Query로 분리할 수 있다면 분리하는 것이 좋다. 마이그레이션 관점에서는 API Endpoint 수준에서만 분리돼도 충분하다. Endpoint 수준에서의 분리가 말은 쉬워보이지만 다음과 같이 데이터 조회와 수정에 대한 API URL이 동일하게 구성되고 HTTP Method만 다르게 돼 있는 경우에는,</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">- 특정 해산물 조회: /api/seafoods/&#123;id&#125; GET</span><br><span class="line">- 특정 해산물 수정: /api/seafoods/&#123;id&#125; PUT</span><br></pre></td></tr></table></figure><p>애플리케이션 서버 앞단에 있는 Reverse Proxy에서 API 하나하나마다 HTTP Method 수준의 분기를 구성해야 하므로 굉장히 고된 작업일 수 있다.</p><p><img src="https://i.imgur.com/ZzyAanE.jpg" alt="Imgur"></p><p>따라서 고된 분리 작업을 거쳐서 전환 과정에서의 안정성을 높일지, 아니면 고된 분리 작업을 생략하는 대신 전환 과정에서 발생할 수 있는 위험을 감수하고 한 방에 전환할지 신중하게 고민하고 선택해야 한다.</p><p>하지만 API가 애초부터 다음과 같이 구성돼 있었다면 아주 자연스럽게 Command와 Query를 분리 전환할 할 수 있다. </p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">- 특정 해산물 조회: /query/api/seafoods/&#123;id&#125; GET</span><br><span class="line">- 특정 해산물 수정: /command/api/seafoods/&#123;id&#125; PUT</span><br></pre></td></tr></table></figure><p>다행스럽게도 OLD 애플리케이션은 위와 같이 Command와 Query가 분리돼 있었다.</p><p><img src="https://i.imgur.com/hCjQ43f.jpg" alt="Imgur"></p><p>서론이 길었는데 위와 같은 현황과 요구사항을 반영해서 구체화된 설계도는 다음과 같다.</p><h1 id="전환-작업-설계"><a href="#전환-작업-설계" class="headerlink" title="전환 작업 설계"></a>전환 작업 설계</h1><p>그림에 간략한 설명이 있으니 그냥 주욱 살펴보자.</p><h3 id="1-현황"><a href="#1-현황" class="headerlink" title="1. 현황"></a>1. 현황</h3><p><img src="https://i.imgur.com/1POOzAp.png" alt="Imgur"></p><h3 id="2-데이터-마이그레이션"><a href="#2-데이터-마이그레이션" class="headerlink" title="2. 데이터 마이그레이션"></a>2. 데이터 마이그레이션</h3><p><img src="https://i.imgur.com/7iLeHNP.png" alt="Imgur"></p><h3 id="3-NEW-Query-애플리케이션-배포"><a href="#3-NEW-Query-애플리케이션-배포" class="headerlink" title="3. NEW Query 애플리케이션 배포"></a>3. NEW Query 애플리케이션 배포</h3><p><img src="https://i.imgur.com/1cmjqcV.png" alt="Imgur"></p><h3 id="4-Query-라우팅-전환"><a href="#4-Query-라우팅-전환" class="headerlink" title="4. Query 라우팅 전환"></a>4. Query 라우팅 전환</h3><p><img src="https://i.imgur.com/Pnaf5mM.png" alt="Imgur"></p><h3 id="5-NEW-Command-애플리케이션-배포"><a href="#5-NEW-Command-애플리케이션-배포" class="headerlink" title="5. NEW Command 애플리케이션 배포"></a>5. NEW Command 애플리케이션 배포</h3><p><img src="https://i.imgur.com/aGE0xxC.png" alt="Imgur"></p><h3 id="6-Command-라우팅-전환"><a href="#6-Command-라우팅-전환" class="headerlink" title="6. Command 라우팅 전환"></a>6. Command 라우팅 전환</h3><p><img src="https://i.imgur.com/o1hUGlF.png" alt="Imgur"></p><h3 id="7-DB-복제-중지"><a href="#7-DB-복제-중지" class="headerlink" title="7. DB 복제 중지"></a>7. DB 복제 중지</h3><p><img src="https://i.imgur.com/iJ6Iy3K.png" alt="Imgur"></p><h3 id="8-NEW-API-라우팅-추가"><a href="#8-NEW-API-라우팅-추가" class="headerlink" title="8. NEW API 라우팅 추가"></a>8. NEW API 라우팅 추가</h3><p><img src="https://i.imgur.com/80AUKB0.png" alt="Imgur"></p><h3 id="9-클라이언트-API-변경"><a href="#9-클라이언트-API-변경" class="headerlink" title="9. 클라이언트 API 변경"></a>9. 클라이언트 API 변경</h3><p><img src="https://i.imgur.com/yd1MLVp.png" alt="Imgur"></p><h3 id="10-OLD-라우팅-제거"><a href="#10-OLD-라우팅-제거" class="headerlink" title="10. OLD 라우팅 제거"></a>10. OLD 라우팅 제거</h3><p><img src="https://i.imgur.com/hPVKUbC.png" alt="Imgur"></p><h3 id="11-OLD-API-Endpoint-제거"><a href="#11-OLD-API-Endpoint-제거" class="headerlink" title="11. OLD API Endpoint 제거"></a>11. OLD API Endpoint 제거</h3><p><img src="https://i.imgur.com/JCrhECX.png" alt="Imgur"></p><h3 id="12-마이그레이션-완료"><a href="#12-마이그레이션-완료" class="headerlink" title="12. 마이그레이션 완료"></a>12. 마이그레이션 완료</h3><p><img src="https://i.imgur.com/wI6kcCW.png" alt="Imgur"></p><p>단계별 실제 구현 작업은 다음 편에서 알아보자.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Zero-Downtime-Migration-Design&quot;&gt;&lt;a href=&quot;#Zero-Downtime-Migration-Design&quot; class=&quot;headerlink&quot; title=&quot;Zero Downtime Migration - Design
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
      <category term="SRE" scheme="http://homoefficio.github.io/categories/Technique/SRE/"/>
    
    
      <category term="migration" scheme="http://homoefficio.github.io/tags/migration/"/>
    
      <category term="architecture" scheme="http://homoefficio.github.io/tags/architecture/"/>
    
      <category term="zero downtime" scheme="http://homoefficio.github.io/tags/zero-downtime/"/>
    
  </entry>
  
  <entry>
    <title>helm 초간단 정리</title>
    <link href="http://homoefficio.github.io/2022/03/19/helm-%EC%B4%88%EA%B0%84%EB%8B%A8-%EC%A0%95%EB%A6%AC/"/>
    <id>http://homoefficio.github.io/2022/03/19/helm-초간단-정리/</id>
    <published>2022-03-18T16:08:07.000Z</published>
    <updated>2022-03-19T04:32:12.475Z</updated>
    
    <content type="html"><![CDATA[<h1 id="helm"><a href="#helm" class="headerlink" title="helm"></a>helm</h1><p><a href="https://helm.sh/" target="_blank" rel="noopener">helm</a>은 k8s 패키지 매니저다.</p><p>공식 문서에 흔한 그림 하나 없고 이래저래 영 마음에 들지 않아서 개념적으로 이해하는 데 필요 이상의 노력이 드는 것 같아서 어쩔 수 없이 따로 핵심만 추려서 정리해본다.</p><h1 id="동작-구조"><a href="#동작-구조" class="headerlink" title="동작 구조"></a>동작 구조</h1><p>일단 k8s 패키지 매니저가 뭔지 그림으로 맛을 보자.</p><p>이해하기 쉽게 한 가지 방식만을 골라서 단순화 했으며, 실제로는 물론 여러가지 시나리오, 방식으로 구성할 수 있다.</p><p><img src="https://i.imgur.com/SeEuKqD.png" alt="Imgur"></p><ul><li>개발자가 작성한 Helm Chart 를 <code>helm push</code> 명령으로 <code>Helm Chart Repository</code>에 업로드 한다.</li><li>개발자가 만든 Container Image 를 <code>docker push</code> 명령으로 <code>Container Registry</code>에 업로드 한다.</li><li>개발자가 k8s Control Plane 에 values.yaml 파일을 지정하면서 <code>helm install</code> 명령을 전달하면,<ul><li>values.yaml 에 있는 값이 Helm Chart 에 주입되고 Helm Release 가 생성되고,</li><li>Helm Chart 에 들어있는 Image 정보를 통해 Container Image 를 가져와서 Helm Chart 정보를 토대로 Pod 등 k8s 자원이 생성된다.</li></ul></li></ul><h1 id="주요-용어"><a href="#주요-용어" class="headerlink" title="주요 용어"></a>주요 용어</h1><h2 id="helm-chart"><a href="#helm-chart" class="headerlink" title="helm chart"></a>helm chart</h2><ul><li>k8s 자원 yaml 파일을 만들 수 있는 여러 yaml 템플릿 파일과 설정값이 들어 있는 values.yaml 파일로 이루어진 파일 세트</li><li><p>대략 아래와 같은 파일이 모여있는 디렉터리라고 봐도 크게 틀리지 않는다. 아래 내용은 helm 한글 문서 <a href="https://helm.sh/ko/docs/topics/charts/#차트-파일-구조" target="_blank" rel="noopener">https://helm.sh/ko/docs/topics/charts/#차트-파일-구조</a> 에서 가져왔다.</p>  <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">CHART이름/  </span><br><span class="line">  Chart.yaml          # 차트에 대한 정보를 가진 YAML 파일</span><br><span class="line">  LICENSE             # 옵션: 차트의 라이센스 정보를 가진 텍스트 파일</span><br><span class="line">  README.md           # 옵션: README 파일</span><br><span class="line">  values.yaml         # 차트에 대한 기본 환경설정 값들</span><br><span class="line">  values.schema.json  # 옵션: values.yaml 파일의 구조를 제약하는 JSON 파일</span><br><span class="line">  charts/             # 이 차트에 종속된 차트들을 포함하는 디렉터리</span><br><span class="line">  crds/               # 커스텀 자원에 대한 정의</span><br><span class="line">  templates/          # values와 결합될 때, 유효한 쿠버네티스 manifest 파일들이 생성될 템플릿들의 디렉터리</span><br><span class="line">  templates/NOTES.txt # 옵션: 간단한 사용법을 포함하는 텍스트 파일</span><br></pre></td></tr></table></figure></li><li><p>templates 폴더 안에는 다음과 같은 k8s 자원 yaml 파일이 들어있다. 아래 내용은 helm 한글 문서 <a href="https://helm.sh/ko/docs/chart_template_guide/getting_started/#mycharttemplates-훑어보기" target="_blank" rel="noopener">https://helm.sh/ko/docs/chart_template_guide/getting_started/#mycharttemplates-훑어보기</a> 에서 가져왔다.</p>  <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">NOTES.txt : 차트의 &quot;도움말&quot;. 이것은 helm install 을 실행할 때 사용자에게 표시될 것이다.</span><br><span class="line">deployment.yaml : 쿠버네티스 디플로이먼트를 생성하기 위한 기본 매니페스트</span><br><span class="line">service.yaml : 디플로이먼트의 서비스 엔드포인트를 생성하기 위한 기본 매니페스트</span><br><span class="line">_helpers.tpl : 차트 전체에서 다시 사용할 수 있는 템플릿 헬퍼를 지정하는 공간</span><br></pre></td></tr></table></figure><ul><li><p>예를 들어 deployment.yaml 파일에 아래와 같은 내용이 있고, <code>helm install</code> 실행 시 사용되는 values.yaml 파일에 <code>spring.configLocation</code>이라는 항목(key)이 있다면 그 값을 <code>SPRING_CONFIG_LOCATION</code> 환경변수에 저장한다.</p>  <figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="string">&#123;&#123;-</span> <span class="string">if</span> <span class="string">hasKey</span> <span class="string">.Values.spring</span> <span class="string">"configLocation"</span> <span class="string">&#125;&#125;</span></span><br><span class="line"><span class="attr">  - name:</span> <span class="string">SPRING_CONFIG_LOCATION</span></span><br><span class="line"><span class="attr">    value:</span> <span class="string">'<span class="template-variable">&#123;&#123; toString .Values.spring.configLocation &#125;&#125;</span>'</span></span><br><span class="line">  <span class="string">&#123;&#123;-</span> <span class="string">end&#125;&#125;</span></span><br></pre></td></tr></table></figure></li></ul></li><li><p>values.yaml 파일도 포함돼 있는데 이 파일을 사용할 수도 있고, helm chart 밖에 존재하는 별도의 values.yaml 파일을 지정해서 사용할 수도 있다. 위 그림에 나온 방식은 별도의 values.yaml 파일을 사용하고 있다.</p></li></ul><h2 id="helm-release"><a href="#helm-release" class="headerlink" title="helm release"></a>helm release</h2><ul><li><code>helm install</code> 명령 실행 결과로 생성되며, helm chart의 인스턴스라고 볼 수 있다.</li><li>하나의 helm chart 에<ul><li>서로 다른 values.yaml 을 적용해서 내용적으로 다른 여러 helm release 를 만들 수도 있고,</li><li>동일한 values.yaml 을 적용하되 release 이름을 다르게 지정해서 내용적으로는 동일한 여러 helm release 를 만들 수도 있다.</li></ul></li><li>helm chart 및 values.yaml 파일의 내용 변경을 k8s 자원에 반영해야 할 때 <code>helm upgrade</code> 명령을 통해 release 의 내용을 변경할 수 있다.</li></ul><h1 id="주요-명령"><a href="#주요-명령" class="headerlink" title="주요 명령"></a>주요 명령</h1><p>사실 이런 건 그냥 공식 문서를 보고 알 수 있어야 하는데, helm 문서에 나온 설명에 혼동을 불러일으키는 부분이 많아서 어쩔 수 없이 일부만 정리한다.</p><p>이 명령 사용법만 알면 나머지 다른 명령은 큰 혼동 없이 문서를 보고도 이해할 수 있을 것이다.</p><h2 id="helm-push"><a href="#helm-push" class="headerlink" title="helm push"></a>helm push</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">형식: helm push 올릴chart경로 helmRepo이름</span><br><span class="line">예제: helm push deploy/my-chart my-repo</span><br></pre></td></tr></table></figure><ul><li>git push 와 비슷하다고 보면 된다.</li><li>helm chart를 helm repository에 저장한다.</li></ul><h2 id="helm-install"><a href="#helm-install" class="headerlink" title="helm install"></a>helm install</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">형식: helm install -n k8sNamespace 생성될release이름 사용할chart이름 -f 사용할valuesyaml파일경로</span><br><span class="line">예제: helm install -n my-namespace my-release my-repo/my-chart -f deploy/values-my-values.yaml</span><br></pre></td></tr></table></figure><ul><li>package.json 내용 대로 패키지를 가져와서 설치하는 npm install 과 비슷하다고 보면 된다.</li><li>helm repository에 저장된 helm chart를 가져오고,</li><li>명시적으로 지정한 values-xxx.yaml 에 있는 값을 helm chart 에 포함돼 있는 여러 k8s yaml template 파일에 주입해서,</li><li>k8s 자원 yaml을 생성할 수 있는 helm release 를 생성하고,</li><li>helm chart 에 지정돼 있는 위치에서 컨테이너 이미지를 가져와서 실제로 k8s에 자원을 생성한다.</li><li>복잡해 보이지만 위 그림을 다시 보면 더 쉽게 이해할 수 있다.</li></ul><h2 id="helm-uninstall"><a href="#helm-uninstall" class="headerlink" title="helm uninstall"></a>helm uninstall</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">형식: helm uninstall -n k8sNamespace 삭제할release이름</span><br><span class="line">예제: helm uninstall -n my-namespace my-release</span><br></pre></td></tr></table></figure><ul><li>install 에 의해 생성된 helm release를 삭제하고 k8s에 생성됐던 deployment 등 관련 자원도 모두 삭제된다.</li></ul><h2 id="helm-upgrade"><a href="#helm-upgrade" class="headerlink" title="helm upgrade"></a>helm upgrade</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">형식: helm upgrade -n k8sNamespace release이름 사용할chart이름 -f 사용할valuesyaml파일경로</span><br><span class="line">예제: helm upgrade -n my-namespace my-release my-repo/my-chart -f deploy/values-my-other-values.yaml</span><br></pre></td></tr></table></figure><ul><li>install 에 의해 생성된 helm release의 내용을 변경한다.</li></ul><h1 id="실무-사용-참고"><a href="#실무-사용-참고" class="headerlink" title="실무 사용 참고"></a>실무 사용 참고</h1><ul><li>소스 코드에서 컨테이너 이미지를 만들어 컨테이너 레지스트리에 업로드 하는 일은 보통 jenkins 등 CI 도구를 사용해서 처리한다.</li><li>소스 코드가 공개 repo에 있다면 ArgoCD 를 k8s 클러스터 내부에 구성해서 편리하게 배포할 수 있다.</li><li>소스 코드가 비공개 repo에 있다면 이 비공개 repo에 접근할 수 있는 곳에 CI용 jenkins를 두고, k8s 클러스터 내부에 CD용 jenkins를 둬서, CI용 jenkins가 이미지를 컨테이너 레지스트리에 올린 후에 CD용 jenkins를 호출(HTTP API)해서 배포할 수 있다.</li><li>소스 코드가 변경됐다면 그에 따른 image 만 변경하면 되므로 CI-CD만 하면 되고, helm 작업(install 또는 upgrade)은 다시 할 필요가 없다.</li><li>소스 코드는 변경이 없는데 helm chart나 values.yaml 파일만 변경됐다면 helm upgrade만 다시 하면 배포까지 되고 CI-CD 작업은 다시 할 필요가 없다.</li></ul><h1 id="helm-chart-작성"><a href="#helm-chart-작성" class="headerlink" title="helm chart 작성"></a>helm chart 작성</h1><p>작성 방법은 공식 문서 <a href="https://helm.sh/ko/docs/chart_template_guide/" target="_blank" rel="noopener">https://helm.sh/ko/docs/chart_template_guide/</a> 를 참고한다.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;helm&quot;&gt;&lt;a href=&quot;#helm&quot; class=&quot;headerlink&quot; title=&quot;helm&quot;&gt;&lt;/a&gt;helm&lt;/h1&gt;&lt;p&gt;&lt;a href=&quot;https://helm.sh/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;helm
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="kubernetes" scheme="http://homoefficio.github.io/tags/kubernetes/"/>
    
      <category term="k8s" scheme="http://homoefficio.github.io/tags/k8s/"/>
    
      <category term="쿠버네티스" scheme="http://homoefficio.github.io/tags/%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4/"/>
    
      <category term="helm" scheme="http://homoefficio.github.io/tags/helm/"/>
    
      <category term="헬름" scheme="http://homoefficio.github.io/tags/%ED%97%AC%EB%A6%84/"/>
    
  </entry>
  
  <entry>
    <title>Counter-Intuitive Reactive Streams</title>
    <link href="http://homoefficio.github.io/2021/11/28/Counter-Intuitive-Reactive-Streams/"/>
    <id>http://homoefficio.github.io/2021/11/28/Counter-Intuitive-Reactive-Streams/</id>
    <published>2021-11-28T09:48:28.000Z</published>
    <updated>2022-03-18T16:07:46.206Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Counter-Intuitive-Reactive-Streams"><a href="#Counter-Intuitive-Reactive-Streams" class="headerlink" title="Counter-Intuitive Reactive Streams"></a>Counter-Intuitive Reactive Streams</h1><p>비동기 프로그래밍은 늘 어렵다.</p><p>이미 오래된 애기지만 Reactive Streams의 구현체인 RxJava나 Reactor가 나오고 Spring에서도 WebFlux가 나오면서 저변이 더욱 확대된 것 같다.<br>학습에 의해 넘을 수 있다고는 하지만 그것도 일부 잘 하는 개발자들에 대한 얘기같고, 현실적으로는 나같은 일반적인 개발자의 직관에 반하는 부분들이 많아 여전히 어렵고 고통스럽다.<br>저변이 확대되는 것은 좋지만, 일부 개발자는 어쩌면 겉멋을 부리려고 굳이 쓰지 않아도 되는 곳에 사용하고, 이력서에 Reactor/Reactive 개발 경험을 추가하고 그걸 발판 삼아 회사를 떠난다.<br>고통은 남아서 이어 받는 사람들의 몫..</p><p>자바라면 아직까지는 답이 없고(토비님이 알려주신 <a href="https://github.com/vsilaev/tascalate-async-await" target="_blank" rel="noopener">https://github.com/vsilaev/tascalate-async-await</a> 같은 서드파티 라이브러리가 있기는 하다),<br>코틀린이라면 다행히 코루틴(coroutine)이 있다.</p><p>둘을 비교해서 코루틴이 쉽고 직관적이라는 자료는 이미 많이 있지만, 그냥 하나 더 덧붙여본다.</p><h2 id="Reactor"><a href="#Reactor" class="headerlink" title="Reactor"></a>Reactor</h2><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">fluxRangeSample</span><span class="params">()</span></span>: Mono&lt;String&gt; &#123;</span><br><span class="line">    <span class="keyword">val</span> strList: MutableList&lt;String&gt; = mutableListOf()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> Flux.range(<span class="number">0</span>, <span class="number">2</span>)</span><br><span class="line">        .doOnNext &#123; outer -&gt;</span><br><span class="line">            println(<span class="string">"outer loop index: <span class="variable">$outer</span>"</span>)</span><br><span class="line">    </span><br><span class="line">            Flux.range(<span class="number">1</span>, <span class="number">4</span>)</span><br><span class="line">                .subscribe &#123; inner -&gt;</span><br><span class="line">                    println(<span class="string">"  inner loop i: <span class="variable">$inner</span>"</span>)</span><br><span class="line">                    </span><br><span class="line">                    Mono.just(<span class="string">"    DB 호출 없는 Mono: <span class="variable">$inner</span>"</span>)</span><br><span class="line">                        .subscribe &#123; anyStr -&gt; println(anyStr) &#125;</span><br><span class="line">                    </span><br><span class="line">                    memberRepository.findById(<span class="string">"moid117"</span>)</span><br><span class="line">                        .subscribe &#123; member -&gt;</span><br><span class="line">                            println(<span class="string">"    DB 호출 있는 Mono: <span class="subst">$&#123;member.oid&#125;</span> - <span class="subst">$&#123;member.name&#125;</span>"</span>)</span><br><span class="line">                            strList.add(<span class="string">"<span class="subst">$&#123;member.oid&#125;</span> - <span class="subst">$&#123;member.name&#125;</span>"</span>)</span><br><span class="line">                        &#125;</span><br><span class="line">                &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        .doOnNext &#123;</span><br><span class="line">            println(<span class="string">"Second doOnNext"</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        .doOnComplete &#123;</span><br><span class="line">            <span class="keyword">for</span> (i <span class="keyword">in</span> <span class="number">1.</span><span class="number">.5</span>) &#123;</span><br><span class="line">                println(<span class="string">"doOnComplete i: <span class="variable">$i</span>"</span>)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        .then(Mono.just(<span class="string">"[<span class="subst">$&#123;strList.joinToString(" | ")&#125;</span>]"</span>))</span><br></pre></td></tr></table></figure><p>사실 보기만 해도 보고 싶은 마음이 별로 들지 않는다. 하지만 봐야 하니 참고 보면, 대충 2중 루프 돌면서 DB에서 읽은 문자열을 <code>strList</code>에 추가해서 반환하는 코드다.</p><p>그래서 아래와 같은 컨트롤러를 붙여서 호출하면 <code>|</code>로 구분된 문자열 목록이 나올 것이다.</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(<span class="meta-string">"/flux-range"</span>)</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">fluxRange</span><span class="params">()</span></span>: Mono&lt;ResponseEntity&lt;String&gt;&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> memberCommandService.fluxRangeSample()</span><br><span class="line">        .map &#123; ResponseEntity.ok(it) &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>실행해보면 짠~ 하고</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">~ 🦑🍕🍺 ❯ http GET :8080/api/members/flux-range</span><br><span class="line">HTTP/1.1 200 OK</span><br><span class="line">Content-Length: 2</span><br><span class="line">Content-Type: text/plain;charset=UTF-8</span><br><span class="line"></span><br><span class="line">[]</span><br></pre></td></tr></table></figure><p>예상과는 다르게 공허하게 비어있는 리스트가 반환된다. 하지만 로그에 보면 아래와 같이 DB에서 읽은 값도 출력이 된다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">outer loop index: 0</span><br><span class="line">  inner loop i: 1</span><br><span class="line">    DB 호출 없는 Mono: 1</span><br><span class="line">  inner loop i: 2</span><br><span class="line">    DB 호출 없는 Mono: 2</span><br><span class="line">  inner loop i: 3</span><br><span class="line">    DB 호출 없는 Mono: 3</span><br><span class="line">  inner loop i: 4</span><br><span class="line">    DB 호출 없는 Mono: 4</span><br><span class="line">Second doOnNext</span><br><span class="line">outer loop index: 1</span><br><span class="line">  inner loop i: 1</span><br><span class="line">    DB 호출 없는 Mono: 1</span><br><span class="line">  inner loop i: 2</span><br><span class="line">    DB 호출 없는 Mono: 2</span><br><span class="line">  inner loop i: 3</span><br><span class="line">    DB 호출 없는 Mono: 3</span><br><span class="line">  inner loop i: 4</span><br><span class="line">    DB 호출 없는 Mono: 4</span><br><span class="line">Second doOnNext</span><br><span class="line">doOnComplete i: 1</span><br><span class="line">doOnComplete i: 2</span><br><span class="line">doOnComplete i: 3</span><br><span class="line">doOnComplete i: 4</span><br><span class="line">doOnComplete i: 5</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br></pre></td></tr></table></figure><p>‘DB 호출 없는 Mono’는 예상대로 로그에 찍혀 있지만, 함께 찍힐 것이라고 기대했던 ‘DB 호출 있는 Mono’는 가장 마지막에 찍혀 있다.</p><p>코드를 보면 DB 조회를 모두 완료한 후에 이름처럼 <code>doOnComplete(...)</code>과 <code>then(...)</code>이 실행될 것 같지만, 실제로는 DB 조회 결과 후 리스트에 추가하는 부분이 가장 나중에 실행된 것이다.<br><strong>심지어 메서드가 반환된 다음에 실행되기 때문에 비어있는 리스트가 반환됐다.</strong></p><p>그뿐만이 아니다. <code>Flux.range(0, 2).doOnNext(...)</code>는 예상대로 0, 1만 반복하지만, <code>Flux.range(1, 4).subscribe(...)</code>는 예상과 달리 1, 2, 3에서 끝나지 않고 4까지 반복된다. <strong>range()의 두 번째 인자가 inclusive 인지 exclusive 인지도 그때그때 달라요 처럼 보인다.</strong> - 추가 Flux.range()는 Flux.range(start, count)라서 이렇게 동작한다고 양봉수 님께서 알려주셨다. 덕분에 왜 저렇게 동작하는지는 알게 됐지만 IntStream.range(startInclusive, endExclusive)를 비롯해서 비슷한 API들의 2번째 인자가 대부분 endExclusive라는 걸 감안하면 Flux.range()는 잘 알려진 관습과 달라서 직관성이 떨어지며 불필요한 학습을 요구하고 있다.</p><p>이처럼 직관과 다른 부분이 Reactor(RxJava도 마찬가지) 사용을 힘들게 만든다. 물론 앞서 말했 듯 학습을 통해 제대로 된 사용법을 익히면 <code>|</code>로 구분된 문자열을 반환하도록 수정할 수 있겠지만 그 학습 비용이 만만치 않다. 게다가 대안이 있다면 더더욱 그 비용은 낭비다.</p><h2 id="Coroutine"><a href="#Coroutine" class="headerlink" title="Coroutine"></a>Coroutine</h2><p>똑같은 일을 하는 코루틴 코드를 보자. <code>suspend</code>와 <code>awaitSingle()</code>을 제외하면 늘 봐오던 코드와 다를 게 없이 편안하다.</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">asyncAwaitRange</span><span class="params">()</span></span>: String &#123;</span><br><span class="line">    <span class="keyword">val</span> strList: MutableList&lt;String&gt; = mutableListOf()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (outer <span class="keyword">in</span> <span class="number">0.</span><span class="number">.1</span>) &#123;</span><br><span class="line">        println(<span class="string">"outer loop index: <span class="variable">$outer</span>"</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (inner <span class="keyword">in</span> <span class="number">1.</span><span class="number">.4</span>) &#123;</span><br><span class="line">            println(<span class="string">"  inner loop i: <span class="variable">$inner</span>"</span>)</span><br><span class="line">            println(<span class="string">"    DB 호출 없는 Mono: <span class="variable">$inner</span>"</span>)</span><br><span class="line">            <span class="keyword">val</span> member = memberRepository.findById(<span class="string">"moid117"</span>).awaitSingle()</span><br><span class="line">            println(<span class="string">"    DB 호출 있는 Mono: <span class="subst">$&#123;member.oid&#125;</span> - <span class="subst">$&#123;member.name&#125;</span>"</span>)</span><br><span class="line">            strList.add(<span class="string">"<span class="subst">$&#123;member.oid&#125;</span> - <span class="subst">$&#123;member.name&#125;</span>"</span>)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        println(<span class="string">"Second doOnNext"</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i <span class="keyword">in</span> <span class="number">1.</span><span class="number">.5</span>) &#123;</span><br><span class="line">        println(<span class="string">"doOnComplete i: <span class="variable">$i</span>"</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="string">"[<span class="subst">$&#123;strList.joinToString(" | ")&#125;</span>]"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>아래와 같은 컨트롤러를 붙여서,</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(<span class="meta-string">"/async-await-range"</span>)</span></span><br><span class="line">suspend <span class="function"><span class="keyword">fun</span> <span class="title">asyncAwaitRange</span><span class="params">()</span></span>: ResponseEntity&lt;String&gt; &#123;</span><br><span class="line">    <span class="keyword">return</span> ResponseEntity.ok(memberCommandService.asyncAwaitRange())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>아래와 같이 호출해보면 의도했던 것처럼 문자열 목록이 나온다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">~ 🦑🍕🍺 ❯ http GET :8080/api/members/async-await-range</span><br><span class="line">HTTP/1.1 200 OK</span><br><span class="line">Content-Length: 151</span><br><span class="line">Content-Type: text/plain;charset=UTF-8</span><br><span class="line"></span><br><span class="line">[moid117 - 문어 | moid117 - 문어 | moid117 - 문어 | moid117 - 문어 | moid117 - 문어 | moid117 - 문어 | moid117 - 문어 | moid117 - 문어]</span><br></pre></td></tr></table></figure><p>로그도 직관에서 한 치의 벗어남이 없다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">outer loop index: 0</span><br><span class="line">  inner loop i: 1</span><br><span class="line">    DB 호출 없는 Mono: 1</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">  inner loop i: 2</span><br><span class="line">    DB 호출 없는 Mono: 2</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">  inner loop i: 3</span><br><span class="line">    DB 호출 없는 Mono: 3</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">  inner loop i: 4</span><br><span class="line">    DB 호출 없는 Mono: 4</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">Second doOnNext</span><br><span class="line">outer loop index: 1</span><br><span class="line">  inner loop i: 1</span><br><span class="line">    DB 호출 없는 Mono: 1</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">  inner loop i: 2</span><br><span class="line">    DB 호출 없는 Mono: 2</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">  inner loop i: 3</span><br><span class="line">    DB 호출 없는 Mono: 3</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">  inner loop i: 4</span><br><span class="line">    DB 호출 없는 Mono: 4</span><br><span class="line">    DB 호출 있는 Mono: moid117 - 문어</span><br><span class="line">Second doOnNext</span><br><span class="line">doOnComplete i: 1</span><br><span class="line">doOnComplete i: 2</span><br><span class="line">doOnComplete i: 3</span><br><span class="line">doOnComplete i: 4</span><br><span class="line">doOnComplete i: 5</span><br></pre></td></tr></table></figure><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>직관에서 많이 벗어나는 Reactive Streams는 써야만 할 때만 쓰자.</p><p>써야만 할 때가 언제일까?</p><p><a href="https://www.reactive-streams.org/" target="_blank" rel="noopener">https://www.reactive-streams.org/</a> 의 첫 문장에 다음과 같이 나와있다.</p><blockquote><p>Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure.</p></blockquote><p>대충 우림말로 ‘논블로킹 배압을 사용해서 비동기 스트림을 처리하는 표준을 제공한다’ 정도로 보이는데 길진 않지만 금방 이해가 되지는 않는다.</p><p>정확하게 같은 의미는 아닐지라도 앞에서 살펴본 사례에서 이어지는 흐름대로 조금 구체화해서 재해석해보면,</p><blockquote><p><strong>선형적인 처리 흐름인데 그 흐름에 들어있는 단위 과정 중간중간에 자원 낭비를 유발하는 blocking 구간이 있어서 이를 최적화 해야하고 배압(back pressure) 적용이 필요할 때</strong></p></blockquote><p>라고 할 수 있을 것 같다.</p><p>물론 선형적인 흐름이 아니라도 <code>Mono.zip()</code> 등 여러가지 (너무) 다양한 리액티브 연산자를 동원하면서 의도대로 실행되게 작성할 수는 있겠지만, 많은 학습 비용과 삽질을 각오해야 한다. 할 수 있다고 해서 무조건 하는 거랑 대안을 검토해보고 해야만 할 때 하는 거랑은 큰 차이가 있다.</p><p>이 글에서는 언급하지 않았지만 Reactive Streams에서 강조하는 또 한 가지 요소는 배압(back pressure)이다. <strong>배압은 데이터를 발생시키는 Publisher(Producer)에게 Subscriber(Consumer)가 처리할 수 있는 만큼만 요청할 수 있게 해주는 메커니즘</strong>을 의미한다.</p><p>일반적인 웹 서비스에서 보면 웹 클라이언트가 서버에 요청을 보내면(Publish), API 서버는 이를 받아서 처리(Consume)하고 응답을 클라이언트에게 반환한다. 이런 흐름에서 API 서버가 처리할 수 있는 만큼만 보내라는 요청을 개별 웹 클라이언트에게 보내는 일은 거의 없다.</p><p>결국 위 코드 사례처럼 <strong>선형적인 처리가 아니라 여러 중첩된 처리 과정이 난무할 때, 그리고 배압 메커니즘이 필요 없을 때, 그러니까 사실 상 대부분의 일반적인 웹 서비스의 경우에는 코루틴을 사용하는 것이 편리하다</strong>고 본다.</p><p>그리고 머지 않아(한 2년 이내?) Virtual Thread가 나온다는 것을 감안하면 Reactive Streams를 사용한 코드는 많은 경우 부담스러운 레거시로 남을 가능성이 크다.</p><p>이미 Reactive Streams로 작성되어 있어 피할 수 없거나, 또는 비동기 스트림 처리와 배압이 정말로 필요한 상황이라서 써야만 한다면, <a href="https://homoefficio.github.io/2021/04/14/Reactive-Streams-with-Sequence-Diagram/">https://homoefficio.github.io/2021/04/14/Reactive-Streams-with-Sequence-Diagram/</a> 여기에서 시작해보자.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Counter-Intuitive-Reactive-Streams&quot;&gt;&lt;a href=&quot;#Counter-Intuitive-Reactive-Streams&quot; class=&quot;headerlink&quot; title=&quot;Counter-Intuitive Reacti
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Spring" scheme="http://homoefficio.github.io/tags/Spring/"/>
    
      <category term="Reactor" scheme="http://homoefficio.github.io/tags/Reactor/"/>
    
      <category term="Reactive Streams" scheme="http://homoefficio.github.io/tags/Reactive-Streams/"/>
    
      <category term="Async" scheme="http://homoefficio.github.io/tags/Async/"/>
    
      <category term="Kotlin" scheme="http://homoefficio.github.io/tags/Kotlin/"/>
    
      <category term="Coroutine" scheme="http://homoefficio.github.io/tags/Coroutine/"/>
    
  </entry>
  
  <entry>
    <title>Reactive Streams with Sequence Diagram</title>
    <link href="http://homoefficio.github.io/2021/04/14/Reactive-Streams-with-Sequence-Diagram/"/>
    <id>http://homoefficio.github.io/2021/04/14/Reactive-Streams-with-Sequence-Diagram/</id>
    <published>2021-04-13T15:28:04.000Z</published>
    <updated>2022-03-18T16:07:46.486Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Reactive-Streams-with-Sequence-Diagram"><a href="#Reactive-Streams-with-Sequence-Diagram" class="headerlink" title="Reactive Streams with Sequence Diagram"></a>Reactive Streams with Sequence Diagram</h1><p>1 req == 1 therad인 서블릿 방식의 한계를 뛰어넘기 위해 Spring에서 WebFlux를 내놨다.<br>Spring WebFlux는 내부적으로 <a href="https://projectreactor.io/" target="_blank" rel="noopener">Reactor</a>를 사용하는데, Reactor는 Reactive Streams 구현체다.<br>Reactive Streams는 <a href="https://www.reactive-streams.org/" target="_blank" rel="noopener">홈페이지</a>에 다음과 같이 간단 명료하게 정의돼 있다.</p><blockquote><p>Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure.</p><p>리액티브 스트림은 비동기 스트림 처리 표준을 제공하는 킹왕짱 계획이야. 논블로킹 백프레셔도 있다능!</p></blockquote><p>솔직히 뭔 소린지 모르겠다. 이거 안다고 퇴근 시간이 앞당겨질 것 같지는 않은데 몰라도 너무 모르니 한 번 알아보려 한다.</p><p>이후 나오는 내용은 <a href="https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html" target="_blank" rel="noopener">JDK 9 Flow 예제</a>와 Reactor, Reactive MongoDB에서 구현한 여러 Reactive Streams 구현 내용을 기준으로 작성했으며, Hot Publisher는 배제하고 Cold Publisher만 다룬다.</p><h2 id="등장-인물"><a href="#등장-인물" class="headerlink" title="등장 인물"></a>등장 인물</h2><p>Reactive Streams는 Publisher, Subscriber, Subscription, Processor 달랑 네 개의 인터페이스로 구성돼있다.<br>이 중에서 Processor는 자기만의 고유한 메서드는 없고 단순히 Publisher, Subscriber 두 가지 인터페이스를 상속받는다. Processor 없이 나머지 3개만으로도 Reactive Streams 협력을 구성할 수 있으므로 Processor는 등장 인물에서 제외한다.</p><p>얘네들이 각각 무슨 동작을 가지고 있고 어떻게 협력해서 클라이언트에게 데이터를 반환하는지 한 눈에 알아보자.</p><h2 id="시퀀스-다이어그램"><a href="#시퀀스-다이어그램" class="headerlink" title="시퀀스 다이어그램"></a>시퀀스 다이어그램</h2><p>검색해보면 몇 가지 나오기는 하는데 내가 이해할 수 있을 만큼 마음에 드는 게 없어서 새로 그렸다(개고생 ㅠㅜ).  </p><p>Publisher, Subscriber, Subscription 인터페이스가 가지고 있는 모든 메서드가 표시돼 있다. publisher 의 <code>map, flatMap, zip, ...</code>는 Reactive Streams 명세에 있는 메서드가 아니라 구현체인 Reactor의 Flux에 있는 메서드인데 설명의 편의를 위해 추가했다.</p><p>Reactive Streams를 사용해서 비동기로 데이터를 조회하는 시나리오를 기준으로 작성했으며,<br>실선 화살표는 메서드 호출, 화살표 위는 메서드 이름, 화살표 아래에 괄호로 표시된 건 메서드 인자이며, 점선 화살표는 반환이고 화살표 아래 괄호 없이 표시된 건 반환값이다.</p><p><img src="https://i.imgur.com/kbp9BGI.png" alt="Imgur"></p><h3 id="데이터-핸들러-로직-정의-및-Subscriber-생성"><a href="#데이터-핸들러-로직-정의-및-Subscriber-생성" class="headerlink" title="데이터 핸들러 로직 정의 및 Subscriber 생성"></a>데이터 핸들러 로직 정의 및 Subscriber 생성</h3><p>데이터를 요청하는 Client는 데이터를 받아서 어떻게 처리할지, 받는 과정에서 오류를 전달 받으면 어떻게 처리할지, 데이터를 모두 받은 후에 어떤 일을 할지 정해야 할 책임을 가지고 있다. 그런 책임을 각각 <code>nextConsumer</code>, <code>errorConsumer</code>, <code>completeRunnable</code>로 정의해서 이를 바탕으로 <code>subscriber</code>를 생성한다.</p><p>설명의 편의를 위해 <code>subscribe</code> 생성을 가장 먼저 표시했는데 그림을 보면 알 수 있겠지만 반드시 가장 먼저 수행할 필요는 없다. <code>publisher.subscribe(subscriber)</code>를 호출하기 직전에 <code>subscriber</code>를 생성해도 된다.</p><h3 id="Data-Provider에-데이터-요청-및-Publisher-생성"><a href="#Data-Provider에-데이터-요청-및-Publisher-생성" class="headerlink" title="Data Provider에 데이터 요청 및 Publisher 생성"></a>Data Provider에 데이터 요청 및 Publisher 생성</h3><p>클라이언트는 DataProvider에게 데이터를 요청한다. DataProvider는 특정 클래스 이름은 아니고 클라이언트로부터 호출을 받으면 데이터 저장소와 연동해서 실제 데이터를 반환하는 책임이 있는 객체를 의미한다고 보면 된다. 예를 들면 ReactiveMongoOperations(ReactiveMongoTemplate)이나 ReactiveMongoRepository라고 생각하면 된다. 이 DataProvider는 <strong>나중에 데이터를 제공할 수 있도록 콜백을 생성하고 이를 <code>publisher</code>를 생성하면서 주입</strong>해준다. 이 부분 자세한 과정은 맨 아래에서 구경할 수 있다. DataProvider는 생성한 <code>publisher</code>를 클라이언트에게 반환한다.</p><h3 id="구독"><a href="#구독" class="headerlink" title="구독"></a>구독</h3><p>클라이언트는 DataProvider로부터 <code>publisher</code>를 반환 받은 후에는, 나중에 <code>publisher</code>가 발행할 데이터를 받아서 비즈니스 요구에 맞게 가공하는 로직을 추가한다. <code>map</code>, <code>flatMap</code>, <code>zip</code> 등 여러 리액티브 연산자가 이 때 사용된다.</p><p>클라이언트는 데이터 가공 로직 추가를 마친 후에<code>publisher.subscribe(subscriber)</code>를 호출한다. 리액티브 스트림에서 절대 잊어서는 안 될 가장 중요한 특징 중 하나가 바로 <strong>구독하기 전에는 아무 일도 일어나지 않는다</strong>는 점이다. 즉 앞에서 아무리 <code>nextConsumer</code>, <code>errorConsumer</code>, <code>completeRunnable</code>를 모두 정의하고, DataProvider를 호출해서 데이터를 가져오고 가공하는 로직을 구현해뒀다 하더라도, <code>publisher.subscribe(subscriber)</code>를 호출하지 않으면 앞서 만든 모든 것들은 전혀 실행되지 않는다. 더 정확하게 말하면 <strong>데이터를 가져오는 로직은 아직 콜백에 담겨 있을 뿐이고, 구독하기 전에는 콜백이 실행되지 않는다.</strong>(엄밀하게는 Hot Publisher인 경우 구독과 상관 없이 데이터를 뿜어내지만 앞서 얘기했듯이 Hot은 너무 뜨거워서 생략)</p><h3 id="Subscription-생성"><a href="#Subscription-생성" class="headerlink" title="Subscription 생성"></a>Subscription 생성</h3><p><code>publisher.subscribe(subscriber)</code>가 호출되면 <code>publisher</code>는 인자로 전달받은 <code>subscriber</code>와 자신이 생성될 때 주입받은 <code>dataCallback</code>를 바탕으로 <code>subscription</code>을 생성하고, <code>subscriber.onSubscribe(subscription)</code>를 호출해서 <code>subscription</code>을 <code>subscriber</code>에게 전달해준다.</p><h3 id="Subscription에-데이터-요청"><a href="#Subscription에-데이터-요청" class="headerlink" title="Subscription에 데이터 요청"></a>Subscription에 데이터 요청</h3><p><code>subscriber</code>는 <code>onSubscribe(subscription)</code>을 통해 <code>subscription</code>을 전달받으면 <code>subscription.request(numOfData)</code>를 호출해서 데이터를 요청한다. 자신이 소화할 수 있을 만큼의 데이터만 요청할 수 있으므로 back pressure 개념이 이 지점에서 발동한다. 그리고 실제 데이터 접근도 이 시점에서 이루어진다.</p><h3 id="실제-데이터-접근-및-onNext-onError-onComplete-호출"><a href="#실제-데이터-접근-및-onNext-onError-onComplete-호출" class="headerlink" title="실제 데이터 접근 및 onNext/onError/onComplete 호출"></a>실제 데이터 접근 및 onNext/onError/onComplete 호출</h3><p><code>subscription</code>은 자신이 생성될 때 주입 받은 콜백을 호출해서 <code>numOfData</code>만큼만 데이터를 가져오고 <code>subscriber.onNext(data)</code>를 반복 호출해서 <code>subscriber</code>에게 데이터를 전달한다. 이 과정에서 오류가 발생하면 <code>subscriber.onError(throwable)</code>로 오류를 <code>subscriber</code>에게 전달하고, 데이터 전달이 정상적으로 완료되면 <code>subscriber.onComplete()</code>를 호출하며 협력이 끝난다.</p><h2 id="비동기는-대체-어디에"><a href="#비동기는-대체-어디에" class="headerlink" title="비동기는 대체 어디에?"></a>비동기는 대체 어디에?</h2><p>리액티브 스트림이 백프레셔와 함께 비동기 스트림 처리 표준을 제공하는 킹왕짱 계획이라고 했는데, 이 협력 구조 상에서 비동기 처리는 어디에 있는 걸까?</p><p>사실 리액티브 스트림이 비동기 스트림 처리 표준 제공 어쩌구라고는 하지만 4가지 인터페이스를 보면 비동기 관련 내용은 전혀 없다. 다시 말해 비동기 처리 없이 동기 처리만 사용하더라도 스트림을 리액티브 방식으로 처리하는 것이 가능하다. 결국 <strong>리액티브 스트림은 비동기 처리 표준을 지향하긴 하지만 그렇다고 비동기를 강제하는 것도 아니다.</strong> 따라서 비동기 처리는 실질적으로는 구현에 달려 있다.</p><p>Reactor의 비동기 처리 관련 규약은 <code>reactor.core.scheduler.Scheduler</code> 인터페이스에 담겨 있다. <code>reactor.core.publisher</code> 패키지에서 Scheduler가 사용되는 곳을 검색하면 어떻게 비동기 처리를 하는지 대략 감을 잡을 수 있을 것이다.</p><p>또 다른 예로 <a href="https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html" target="_blank" rel="noopener">JDK 9 예제</a>에 보면 <code>Subscription</code>이 <code>Subscriber</code>의 <code>onNext()</code>를 호출할 때 <code>Executor</code>를 이용해서 비동기로 호출하고 있다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><blockquote><p>Reactive Streams는 논블로킹, 백프레셔를 포함해서 스트림을 비동기 방식으로 처리할 수 있는 표준 API다.</p><p>Publisher, Subscriber, Subscription 이 서로 협력하면서 스트림을 리액티브 방식으로 처리한다.<br>자세한 협력 구조는 글보다는 <a href="https://i.imgur.com/kbp9BGI.png" target="_blank" rel="noopener">시퀀스 다이어그램</a>을 참고하면 더 쉽게 이해할 수 있다.</p></blockquote><hr><h1 id="번외편"><a href="#번외편" class="headerlink" title="번외편"></a>번외편</h1><p>사실 여기까지만 알면 리액티브 스트림의 협력 구조를 이해하는 데는 충분하다고 생각한다.<br>물론 그렇다고 리액티브 스트림을 사용하는 데 충분하다는 얘기는 아니다. 실무적으로는 <code>map</code>, <code>flatMap</code>, <code>concatMap</code>, <code>zip</code> 등등 엄~~청나게 많은 리액티브 연산자들 예쁘디 예쁜 마블 다이어그램 보면서 다 공부해야 된다능~~ ㄷㄷㄷ</p><p>암튼 원래는 이 정도만 알고 넘어가려고 했는데.. 이 정도 알아보고 나니 또 다른 궁금증이 파생되어..<br>아래에 이어지는 건 굳이 안 봐도 된다. 그냥 개인적인 주절거림일 뿐이다.</p><h2 id="이런-이름-적절한가"><a href="#이런-이름-적절한가" class="headerlink" title="이런 이름 적절한가?"></a>이런 이름 적절한가?</h2><p><code>Publisher</code>를 보자. 퍼블리셔라면 뭔가 데이터를 발행하고 뿜어내야 할 것 같은데 정작 가진 메서드 이름이나 파라미터는 영 다르다. 달랑 하나 있는 메서드는 다음과 같다.</p><blockquote><p><code>Publisher.subscribe(Subscriber)</code></p><p>발행자가 구독한다 구독자를. 읭? 발행자가 구독자를 구독한다고? 뭥미?</p></blockquote><p>일단 구독은 구독자의 행위인데 구독자가 아니라 엉뚱하게 발행자가 주어로 나와있다. 사실 이렇게 행위의 주어와 메서드가 소속된 객체가 다르게 돼 있는 API는 많다. 모든 getter 메서드는 get이라는 행위의 주어와 getXXX라는 메서드가 소속된 객체가 다르다. 멀리 갈 것 없이 위 시퀀스 다이어그램에 나오는 <code>subscription.request(numOfData)</code>도 마찬가지다. request 행위의 주어는 <code>subscriber</code>지만 <code>request()</code> 메서드는 <code>subscription</code>에 소속돼 있다. 그래도 이 코드를 읽는 데는 전혀 불편함이 없다. 결국 <strong>행위의 주어와 메서드가 소속된 객체가 다르다는 것만으로는 불편함을 느끼지 않는다.</strong></p><p>하지만 <strong><code>Publisher.subscribe(Subscriber)</code>는 단순히 행위의 주어와 메서드가 소속된 객체가 불일치하는 데 그치지 않고, 발행자/구독자라는 대칭 관계에 있는 애들이 구독한다(subscribe)라는 행위(동사)를 중심으로 주어, 목적어가 뒤바뀌어 있다.</strong> 그래서 직관적으로 자연스럽게 협력 구조를 이해하는 데 큰 걸림돌이 된다.</p><p>단순히 문장 해석하듯 자연스럽게 이해되지 않는 것뿐만 아니라 불편한 이유는 몇 가지 더 있다.</p><p>먼저 이 <code>subscribe</code>라는 이름은 실제 동작과도 부합하지 않는다.</p><p>JDK API 문서에 나오는 <a href="https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.Publisher.html#subscribe-java.util.concurrent.Flow.Subscriber-" target="_blank" rel="noopener"><code>Publisher.subscribe(Subscriber)</code>에 대한 설명</a>은 다음과 같은 간단하고 명료한 문장으로 시작한다.</p><blockquote><p>Adds the given Subscriber if possible.</p></blockquote><p>즉 <strong>실제 하는 동작은 Subscriber를 add 하는 건데 subscribe 라는 엉뚱한 이름이 사용되고 있다.</strong></p><p>게다가 Reactive는 Push 기반 데이터 처리 패턴이라고 할 수 있는데, 데이터 처리 실행을 유발하는 핵심 동작을 구독자 입장의 동작인 <code>subscribe</code>라고 이름지어버리니까 Publisher가 Push하는 패턴이 아니라 Subscriber가 Pull하는 패턴처럼 느껴진다. 어쩌면 Push를 퇴색시키는 이 부분이 Reactive를 이해하고 사용하는 데 있어 가장 나쁜 점일 수도 있겠다. 이쯤되면 불편함을 넘어 해로움이라고 할 수도 있겠다.</p><p>불편했던 또 한 가지 이유는 onSubscribe, onNext, onError, onComplete 메서드의 컨텍스트인 Subscriber는 주어의 역할을 하는데, Subscriber와 대칭 관계라고 할 수 있는 Publisher는 subscribe 메서드의 주어도 아니고 목적어도 아니니 대칭성이 깨져서 직관성이 떨어지기 때문이다.</p><p>암튼 내가 보기엔 불편함을 넘어 해로워보이기까지 하는 <code>subscribe(Subscriber)</code>가 사용된 이유는 아마도 답정너처럼 Reactive Streams 협력 구조에서 <code>subscribe</code>라는 용어를 중심으로 내세우고 싶었던 것이 아니었을까 짐작해본다.</p><p>예전에도 이 부분이 마음에 안 들어서 <a href="https://www.facebook.com/hanmomhanda/posts/10214512128821140" target="_blank" rel="noopener">https://www.facebook.com/hanmomhanda/posts/10214512128821140</a> 여기에 끄적여둔 게 있다. 댓글들을 보면 나를 제외한 나머지 모두는 <code>Publisher.subscribe(Subscriber)</code> 이게 어색하지도 해석하는 데 불편하지도 않다는 반응이라서 신기하기도 했다.</p><p><code>Publisher.subscribe(Subscriber)</code>가 불편하다면, Publisher/Subscriber와 비슷하게 Producer/Consumer 관계가 사용되는 <code>Stream.forEach(Consumer)</code>도 불편해야 하는 거 아니냐는 날카로운 의견도 있었는데, 일단 Stream이 역할은 Producer 이긴 하지만 API 상 공식 이름은 어디까지나 Stream이다. Stream.forEach(Consumer)는 스트림 안에서 흐르는 각 원소에 대해 Consumer가 소비한다 외에 다른 해석을 떠올리기 어려울 정도로 직관적이므로 <code>Publisher.subscribe(Subscriber)</code>와는 많이 다르다고 생각한다.</p><h2 id="cancel은"><a href="#cancel은" class="headerlink" title="cancel은?"></a>cancel은?</h2><p>실제 구독을 중단하는 일을 수행하는 책임은 현재 Subscription에 있다. 그리고 <code>Subscription.cancel()</code>를 누가 호출하는지 구현체를 살펴보니 본 것 중에서는 모두 Subscriber가 호출하고 있다. 하지만 현재 설계 상으로는 Subscriber에 cancel 관련 공개 메서드가 없으므로, cancel 여부를 결정하는 클라이언트가 cancel을 유발할 수 있는 방법이 없다.</p><p>그럼 도대체 cancel이 어떻게 실현될 수 있는 걸까? 구현체인 Reactor를 살펴보니, Reactive Streams 에서는 <code>Publisher.subscribe()</code>는 리턴 값이 없지만 리액터의 Publisher 구현체인 Flux에는 Disposable을 반환하는 <code>subscribe()</code> 메서드가 있다. 이 Disposable에 <code>dispose()</code> 메서드가 있어서 이를 통해 <code>Subscription.cancel()</code>을 호출할 수 있다. 그런데 Disposable도 Reactive Streams에는 없고 구현체인 Reactor에서 만들어 사용하는 인터페이스다.</p><p>요는 Reactive Streams만으로는 cancel 여부를 결정하는 클라이언트가 cancel 할 수 있는 방법이 없다.</p><h2 id="개선된-시퀀스-다이어그램"><a href="#개선된-시퀀스-다이어그램" class="headerlink" title="개선된 시퀀스 다이어그램"></a>개선된 시퀀스 다이어그램</h2><p>궁시렁대기만 할 게 아니라 개선도 한 번 생각해보자.</p><p><code>subscribe</code>라는 용어를 중심에 두고 싶었다면 협력 구조도 좀 바꿔서 <code>Subscriber.subscribe(Publisher)</code>로 했으면 어땠을까? 이렇게 하면 클라이언트가 <code>Subscriber</code>와만 직접 통신하게 되고, 너무 구독만 강조돼서 Push 기반이라는 Reactive의 특징이 퇴색되는 안 좋은 결과로 이어진다. 게다가 코드도 아래와 같이 못난이가 돼버린다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">subscriber.subscribe(</span><br><span class="line">    Flux.just(<span class="string">"a"</span>, <span class="string">"b"</span>, <span class="string">"c"</span>)</span><br><span class="line">        .filter(...)</span><br><span class="line">        .map(...)</span><br><span class="line">        ...</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>결국 컨텍스트인 <code>Publisher</code>를 <code>Subscriber</code>로 바꾸는 건 좋은 대안이 아닌 걸로 보인다. 그렇다면 <code>Publisher</code>는 그대로 두고 <code>subscribe(Subscriber)</code> 대신 실제 동작에 맞게 <code>add(Subscriber)</code>로 하거나, add가 너무 일반적인 동사라 의도를 표현하는 데 부족하다면 <code>addSubscriber(Subscriber)</code>로 했다면 훨씬 이해하기 편했을 것 같다. 그런데 이렇게 하면 ‘구독하지 않으면 아무 일도 일어나지 않는다’라는 문장과 어긋나는 면이 있고, 여전히 계속 구독만 강조되는 것처럼 보인다.</p><p>‘추가’라는 표면적인 동작과는 맞지 않지만 결국에는 구독자에게 발행한다는 의미에 무게를 둬서 <code>publishTo(Subscriber)</code>로 하면 어떨까? 일단 이렇게 하면 ‘발행자가 구독자에게 발행한다’라고 아주 직관적으로 이해할 수 있게 된다. 게다가 Push 모델이라는 점도 훨씬 극명하게 드러난다. ‘구독하지 않으면 아무 일도 일어나지 않는다’ 대신에 ‘발행하지 않으면 아무 일도 일어나지 않는다’로 바꿔도 전혀 어색하지 않다.</p><p>이름 하나 바꿨을뿐인데 모든 게 좋아진 것 같(은 환각이 든)다. cancel도 Publisher에게 추가하는 것이 좋겠다.</p><p>이를 반영해서 개선한 시퀀스 다이어그램은 다음과 같다. 딸기색 둥근 네모 부분만 달라졌다.</p><p><img src="https://i.imgur.com/84eEoAS.png" alt="Imgur"></p><h2 id="비동기-처리-관점에서-리액티브-스트림의-가치"><a href="#비동기-처리-관점에서-리액티브-스트림의-가치" class="headerlink" title="비동기 처리 관점에서 리액티브 스트림의 가치"></a>비동기 처리 관점에서 리액티브 스트림의 가치</h2><p>리액티브 스트림을 활용한 프로그래밍은 여러모로 진입 장벽이 높다. 그런데 그걸 꼭 넘어서 사용해야할 정도로 가치가 있을까?</p><p>비동기 처리라면 C#, JavaScript, Rust 등에는 async/await, Kotlin에는 coroutine 처럼 더 진입 장벽이 낮은 API가 제공되고 있다. 그리고 자바에도 정확히 언제가 될지는 모르지만 Fiber(Project Loom)가 도입될 예정이므로 <strong>비동기 처리라는 관점에서 리액티브 스트림이나 ReactiveX가 앞서 예를 든 더 간편한 API들과 견주어 경쟁력을 유지할 수 있을지 솔직히 의문</strong>이다. 한 예로 Kotlin Coroutine과 Reactive Streams 코드를 비교한 자료(<a href="https://github.com/HomoEfficio/dev-tips/blob/master/Kotlin-Coroutine-vs-Reactive-Streams(Reactor).md" target="_blank" rel="noopener">https://github.com/HomoEfficio/dev-tips/blob/master/Kotlin-Coroutine-vs-Reactive-Streams(Reactor).md</a>) 를 보면 이런 의문을 가질 법하다는 사실을 실감나게 느낄 수 있을 것이다.</p><p>따라서 비동기 처리 관점에서 리액티브 스트림을 아주 깊게 이해해야만 할 것 같지는 않다. 그저 back pressure를 적용할 수 있어야 하고, <code>onNext</code>, <code>onError</code>, <code>onComplete</code> 와 같이 이벤트 핸들링 방식으로 처리하는 API를 제공하려다보니 이런 설계가 나왔겠지 정도로 털고 가자(아 훈훈해..).</p><p>물론 꼭 <strong>비동기 처리를 필요로 하지 않는 Push 기반의 데이터 처리 패턴으로서의 존재 가치는 여전히 유효</strong>할 것이다.</p><h2 id="콜백이-어디에-어떻게-감춰져-있는지-구경하기"><a href="#콜백이-어디에-어떻게-감춰져-있는지-구경하기" class="headerlink" title="콜백이 어디에 어떻게 감춰져 있는지 구경하기"></a>콜백이 어디에 어떻게 감춰져 있는지 구경하기</h2><p>그럼 Reactor와 Reactive MongoDB가 추상화해서 감춰든 부분을 한 번 구경해보자. 위에서 아래로 호출이 이어진다고 보고 흐름을 구경하면 된다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ReactiveMongoTemplate</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">doFindOne</span><span class="params">(String collectionName, Document query, @Nullable Document fields,</span></span></span><br><span class="line"><span class="function"><span class="params">            Class&lt;T&gt; entityClass, FindPublisherPreparer preparer)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        MongoPersistentEntity&lt;?&gt; entity = mappingContext.getPersistentEntity(entityClass);</span><br><span class="line"></span><br><span class="line">        QueryContext queryContext = queryOperations</span><br><span class="line">                .createQueryContext(<span class="keyword">new</span> BasicQuery(query, fields != <span class="keyword">null</span> ? fields : <span class="keyword">new</span> Document()));</span><br><span class="line">        Document mappedFields = queryContext.getMappedFields(entity, entityClass, projectionFactory);</span><br><span class="line">        Document mappedQuery = queryContext.getMappedQuery(entity);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (LOGGER.isDebugEnabled()) &#123;</span><br><span class="line">            LOGGER.debug(String.format(<span class="string">"findOne using query: %s fields: %s for class: %s in collection: %s"</span>,</span><br><span class="line">                    serializeToJsonSafely(query), mappedFields, entityClass, collectionName));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 여기!!</span></span><br><span class="line">        <span class="keyword">return</span> executeFindOneInternal(<span class="keyword">new</span> FindOneCallback(mappedQuery, mappedFields, preparer),</span><br><span class="line">                <span class="keyword">new</span> ReadDocumentCallback&lt;&gt;(<span class="keyword">this</span>.mongoConverter, entityClass, collectionName), collectionName);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">executeFindOneInternal</span><span class="params">(ReactiveCollectionCallback&lt;Document&gt; collectionCallback,</span></span></span><br><span class="line"><span class="function"><span class="params">            DocumentCallback&lt;T&gt; objectCallback, String collectionName)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 여기!!</span></span><br><span class="line">        <span class="keyword">return</span> createMono(collectionName,</span><br><span class="line">                collection -&gt; Mono.from(collectionCallback.doInCollection(collection)).flatMap(objectCallback::doWith));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">createMono</span><span class="params">(String collectionName, ReactiveCollectionCallback&lt;T&gt; callback)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        Assert.hasText(collectionName, <span class="string">"Collection name must not be null or empty!"</span>);</span><br><span class="line">        Assert.notNull(callback, <span class="string">"ReactiveCollectionCallback must not be null!"</span>);</span><br><span class="line"></span><br><span class="line">        Mono&lt;MongoCollection&lt;Document&gt;&gt; collectionPublisher = doGetDatabase()</span><br><span class="line">                .map(database -&gt; getAndPrepareCollection(database, collectionName));</span><br><span class="line"></span><br><span class="line">                                                                   <span class="comment">// 여기!!</span></span><br><span class="line">        <span class="keyword">return</span> collectionPublisher.flatMap(collection -&gt; Mono.from(callback.doInCollection(collection)))</span><br><span class="line">                .onErrorMap(translateException());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">FindOneCallback</span> <span class="keyword">implements</span> <span class="title">ReactiveCollectionCallback</span>&lt;<span class="title">Document</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> Document query;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> Optional&lt;Document&gt; fields;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> FindPublisherPreparer preparer;</span><br><span class="line"></span><br><span class="line">        FindOneCallback(Document query, <span class="meta">@Nullable</span> Document fields, FindPublisherPreparer preparer) &#123;</span><br><span class="line">            <span class="keyword">this</span>.query = query;</span><br><span class="line">            <span class="keyword">this</span>.fields = Optional.ofNullable(fields);</span><br><span class="line">            <span class="keyword">this</span>.preparer = preparer;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> Publisher&lt;Document&gt; <span class="title">doInCollection</span><span class="params">(MongoCollection&lt;Document&gt; collection)</span></span></span><br><span class="line"><span class="function">                <span class="keyword">throws</span> MongoException, DataAccessException </span>&#123;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (LOGGER.isDebugEnabled()) &#123;</span><br><span class="line"></span><br><span class="line">                LOGGER.debug(<span class="string">"findOne using query: &#123;&#125; fields: &#123;&#125; in db.collection: &#123;&#125;"</span>, serializeToJsonSafely(query),</span><br><span class="line">                        serializeToJsonSafely(fields.orElseGet(Document::<span class="keyword">new</span>)), collection.getNamespace().getFullName());</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">                                                        <span class="comment">// 여기!!</span></span><br><span class="line">            FindPublisher&lt;Document&gt; publisher = preparer.initiateFind(collection, col -&gt; col.find(query, Document.class));</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (fields.isPresent()) &#123;</span><br><span class="line">                publisher = publisher.projection(fields.get());</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> publisher.limit(<span class="number">1</span>).first();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// FindPublisherPreparer</span></span><br><span class="line">    <span class="function"><span class="keyword">default</span> FindPublisher&lt;Document&gt; <span class="title">initiateFind</span><span class="params">(MongoCollection&lt;Document&gt; collection,</span></span></span><br><span class="line"><span class="function"><span class="params">            Function&lt;MongoCollection&lt;Document&gt;, FindPublisher&lt;Document&gt;&gt; find)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        Assert.notNull(collection, <span class="string">"Collection must not be null!"</span>);</span><br><span class="line">        Assert.notNull(find, <span class="string">"Find function must not be null!"</span>);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (hasReadPreference()) &#123;</span><br><span class="line">            collection = collection.withReadPreference(getReadPreference());</span><br><span class="line">        &#125;</span><br><span class="line">               <span class="comment">// 여기!!</span></span><br><span class="line">        <span class="keyword">return</span> prepare(find.apply(collection));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// ReactiveMongoTemplate</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> FindPublisher&lt;Document&gt; <span class="title">prepare</span><span class="params">(FindPublisher&lt;Document&gt; findPublisher)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">            FindPublisher&lt;Document&gt; findPublisherToUse = operations.forType(type) <span class="comment">//</span></span><br><span class="line">                    .getCollation(query) <span class="comment">//</span></span><br><span class="line">                    .map(Collation::toMongoCollation) <span class="comment">//</span></span><br><span class="line">                    .map(findPublisher::collation) <span class="comment">//</span></span><br><span class="line">                    .orElse(findPublisher);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// findPublisherToUse 에 limit, skip, hint 등 적용한 후에 반환</span></span><br><span class="line">            <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> findPublisherToUse;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// FindPublisehrImpl</span></span><br><span class="line"><span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">FindPublisherImpl</span>&lt;<span class="title">TResult</span>&gt; <span class="keyword">implements</span> <span class="title">FindPublisher</span>&lt;<span class="title">TResult</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AsyncFindIterable&lt;TResult&gt; wrapped;</span><br><span class="line"></span><br><span class="line">    FindPublisherImpl(<span class="keyword">final</span> AsyncFindIterable&lt;TResult&gt; wrapped) &#123;</span><br><span class="line">        <span class="keyword">this</span>.wrapped = notNull(<span class="string">"wrapped"</span>, wrapped);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Publisher&lt;TResult&gt; <span class="title">first</span><span class="params">()</span> </span>&#123;</span><br><span class="line">               <span class="comment">// 여기!!</span></span><br><span class="line">        <span class="keyword">return</span> Publishers.publish(wrapped::first);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">//Publishers</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> &lt;TResult&gt; <span class="function">Publisher&lt;TResult&gt; <span class="title">publish</span><span class="params">(<span class="keyword">final</span> Block&lt;SingleResultCallback&lt;TResult&gt;&gt; operation)</span> </span>&#123;</span><br><span class="line">           <span class="comment">// 여기!!</span></span><br><span class="line">    <span class="keyword">return</span> subscriber -&gt; <span class="keyword">new</span> SingleResultCallbackSubscription&lt;&gt;(operation, subscriber);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Reactive-Streams-with-Sequence-Diagram&quot;&gt;&lt;a href=&quot;#Reactive-Streams-with-Sequence-Diagram&quot; class=&quot;headerlink&quot; title=&quot;Reactive Streams
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Spring" scheme="http://homoefficio.github.io/tags/Spring/"/>
    
      <category term="Reactor" scheme="http://homoefficio.github.io/tags/Reactor/"/>
    
      <category term="Reactive Streams" scheme="http://homoefficio.github.io/tags/Reactive-Streams/"/>
    
      <category term="Async" scheme="http://homoefficio.github.io/tags/Async/"/>
    
      <category term="Reactive Streams Sequence Diagram" scheme="http://homoefficio.github.io/tags/Reactive-Streams-Sequence-Diagram/"/>
    
      <category term="Sequence Diagram" scheme="http://homoefficio.github.io/tags/Sequence-Diagram/"/>
    
      <category term="Publisher" scheme="http://homoefficio.github.io/tags/Publisher/"/>
    
      <category term="Subscriber" scheme="http://homoefficio.github.io/tags/Subscriber/"/>
    
      <category term="Subscription" scheme="http://homoefficio.github.io/tags/Subscription/"/>
    
  </entry>
  
  <entry>
    <title>Java Concurrency Evolution</title>
    <link href="http://homoefficio.github.io/2020/12/11/Java-Concurrency-Evolution/"/>
    <id>http://homoefficio.github.io/2020/12/11/Java-Concurrency-Evolution/</id>
    <published>2020-12-11T07:43:41.000Z</published>
    <updated>2022-03-18T16:07:46.342Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Java-Concurrency-Evolution"><a href="#Java-Concurrency-Evolution" class="headerlink" title="Java Concurrency Evolution"></a>Java Concurrency Evolution</h1><p>DZone에서 본 글인데 동시성 처리 관련 여러 방식을 비교하면서, 프로젝트 룸(Loom) 코드도 구경할 수 있어서 원글 작성자의 허락을 받고 우리말로 옮겨본다.</p><blockquote><p>원문: <a href="https://dzone.com/articles/java-concurrency-evolution" target="_blank" rel="noopener">https://dzone.com/articles/java-concurrency-evolution</a></p></blockquote><p>자바는 초창기부터 스레드를 사용해서 동시성 프로그래밍을 할 수 있었다. 자바 1.1 버전까지는 JVM에서 그린 스레드(가상 스레드)를 지원했지만 그 이후 버전에서는 폐기되고 OS 네이티브 스레드를 사용하게 됐다. 하지만 오랫동안 관 속에 묻혀있던 가상 스레드를 다시 부활시키는 프로젝트 룸(Loom)이 수면 위로 부상하면서, 폐기됐던 가상 스레드가 다시 부활하여 주류에 올라설 수 있게 됐다.</p><p>이 글의 목적은 자바의 스레드/동시성 처리 진화 과정에 있었던 주요 마일스톤을 살펴보는 것이다. 스레드와 동시성을 다루는 자료는 차고 넘쳐나므로 이 글의 목적에서 벗어나는 다음 내용은 여기에서는 다루지 않는다.</p><ul><li>에러 처리: 거의 다루지 않으며 코드 가독성을 높이기 위해 Lombok의 <code>@SneakyThrows</code>만 사용한다.</li><li>동시성 라이브러리나 프레임워크: JVM에는 Quasar, Akka, Guava, EA Async 등 동시성을 다루는 라이브러리와 프레임워크가 아주 많다.</li><li>복잡한 종료 조건: 주어진 태스크(task)를 수행하는데 필요한 스레드를 시작하고 나서, 스레드 종료까지 얼마나 기다려야 하는지 항상 명확하지는 않다.</li><li>스레드 간 동기화: 이 얘기를 다뤘다간 글을 끝맺을 수 없을 것만 같다.</li></ul><p>그럼 이 글에서 다루는 건 뭘까? 좋은 질문이다. 재미있는 건 위에서 다 빼버렸는데 무슨 얘기를 하려는 걸까?</p><p>똑같은 일을 하는 예제 하나를 옛날 방식부터 시작해서 자바 동시성 진화 과정을 따라 더 새롭고 색다른 방법으로 구현하면서 그 실행 ‘동작’(behavior)을 비교해보려고 한다. 여기서 다루는 방식이 전부는 아니며 다른 방식도 있다. 자바 언어에서 제공하는 API만으로 구현할 수 있는 방법만을 모아서 살펴보려고 했는데, 주류라고 할 수 있는 리액티브 방식을 외면할 수 없어서 같이 다루기로 한다.</p><h2 id="자바-스레드"><a href="#자바-스레드" class="headerlink" title="자바 스레드"></a>자바 스레드</h2><p>본격적으로 시작하기 전에 자바의 스레드에 대해 몇 가지만 짚고 넘어가자.</p><ul><li>JVM 스레드와 OS 스레드는 1:1로 매핑된다. JVM 스레드는 OS 스레드를 얇게 덧씌워 만든 거라고 볼 수 있다.</li><li>OS는 아주 범용적인(그래서 느린) 스케줄링을 사용한다. OS는 JVM 내부에 대해 아무 것도 알지 못한다.</li><li>스레드를 만들고 이 스레드 저 스레드를 오가는 작업은 커널을 거쳐야 하므로 비용이 많이 든다(느리다).</li><li>OS의 continuation 구현체는 자바 콜 스택(call stack) 뿐만 아니라 네이티브 콜 스택도 포함하며, 자원을 많이 사용한다.</li><li>OS 스레드 갯수는 CPU 코어 숫자에 의해 제약받는다.</li><li>스레드에 사용되는 스택 메모리는 OS에 의해 힙 외의 영역에 마련된다.</li></ul><p><img src="https://i.imgur.com/ferFwji.png" alt="Imgur"></p><h2 id="태스크"><a href="#태스크" class="headerlink" title="태스크"></a>태스크</h2><p>예제에서 수행할 태스크(task)는 주로 동시에 호출되어 실행된다. 사용자별로 다음과 같은 흐름으로 요청을 처리하는 웹 서버를 떠올려보자.</p><ul><li>서비스 A가 호출되면 완료될 때까지 1000ms가 필요하다.</li><li>서비스 B가 호출되면 완료될 때까지 500ms가 필요하다.</li><li>서비스 A, B의 결과는 파일, DB, S3 등 저장 방식별로 Z회 저장되며, 한 번 저장하는데 300ms가 필요하다. 현실에서는 저장 방식 별로 소요 시간이 다르겠지만 계산의 편의를 위해 같다고 가정한다.</li></ul><p><img src="https://i.imgur.com/2WDaPHW.png" alt="Imgur"></p><p>동시 실행이 전혀 없는 No Concurrency 방식에서는 <code>요청 갯수 * (서비스A 처리 시간 + 서비스B 처리 시간 + 저장 횟수 * 저장 시간)</code>만큼의 시간이 필요하다.</p><p>반면에 모든 요청이 동시에 실행되는 이상적인 Full Concurrency 방식에서는 <code>Max(서비스A 처리 시간, 서비스B 처리 시간) + 저장 시간</code> 만큼, 그러니까 1,300ms가 필요하다.</p><p>서비스 처리나 저장에 필요한 시간은 단순하게 <code>Thread.sleep()</code>으로 구현했다. 현실성은 물론 떨어지지만 여러 방식을 비교하고 이해하는 데는 충분하다.</p><p>전체 코드는 <a href="https://github.com/bejancsaba/java-concurrency-evolution" target="_blank" rel="noopener">여기</a>에서 확인할 수 있다. 요청 갯수(N)와 저장 횟수(Z)를 마음대로 바꿔가면서 어떤 결과가 나오는지 살펴보자. 늘 그렇듯이 모든 문제는 여러 가지 방식으로 풀 수 있으며, 깃헙에 있는 코드는 그 중 하나일 뿐이다. 가독성을 위해 최적화를 희생한 코드도 있음을 미리 밝혀둔다. 이제 예제 실행 결과를 살펴보면서 재미나는 자바 동시성 진화 과정을 따라가보자.</p><h2 id="동시성-미사용"><a href="#동시성-미사용" class="headerlink" title="동시성 미사용"></a>동시성 미사용</h2><p>가장 단순한 방식이며, 아주 친숙한 코드다. 이런 코드 작성을 도와주는 도구도 많고, 쉬워서 직관적으로 금방 이해하고 디버깅 할 수 있다. </p><p>하지만 자원을 효율적으로 사용하지 못해서 실행 성능은 떨어진다는 것을 쉽게 알 수 있다. 사용된 유일한 JVM 스레드는 하나의 OS 스레드를 사용하며, 하나의 OS 스레드는 하나의 CPU 코어를 사용하므로 나머지 코어는 모두 놀게 된다. 요청 갯수와 저장 횟수가 많아지면 실행 소요 시간도 계속 늘어난다.</p><p><img src="https://i.imgur.com/OptTx1J.png" alt="Imgur"></p><h3 id="Code"><a href="#Code" class="headerlink" title="Code"></a>Code</h3><p>코드도 아주 직관적이다. 짧고 핵심만 들어있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">shouldBeNotConcurrent</span><span class="params">()</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> user = <span class="number">1</span>; user &lt;= USERS; user++) &#123;</span><br><span class="line">        String serviceA = serviceA(user);</span><br><span class="line">        String serviceB = serviceB(user);</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt;= PERSISTENCE_FORK_FACTOR; i++) &#123;</span><br><span class="line">            persistence(i, serviceA, serviceB);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>helper 메서드 내용은 가독성을 위해 생략했는데 원한다면 <a href="https://github.com/bejancsaba/java-concurrency-evolution/blob/main/src/test/java/com/concurrency/evolution/ConcurrencySupport.java" target="_blank" rel="noopener">여기</a>에서 확인할 수 있다.</p><h3 id="실행-결과"><a href="#실행-결과" class="headerlink" title="실행 결과"></a>실행 결과</h3><p>실행 결과는 앞의 ‘Task’ 단원에 나온 그림 아래 쪽 표의 No Concurrency와 같다.</p><p><img src="https://i.imgur.com/8rrC0JN.png" alt="Imgur"></p><h2 id="네이티브-멀티-스레딩"><a href="#네이티브-멀티-스레딩" class="headerlink" title="네이티브 멀티 스레딩"></a>네이티브 멀티 스레딩</h2><p>멀티 스레딩에는 일반적으로 다음과 같은 난관이 있다.</p><ul><li>CPU 코어나 메모리 같은 자원의 효율적 이용</li><li>세밀한 스레드 갯수 조절이나 스레드 관리</li><li>제어 흐름과 컨텍스트 유실</li><li>실행 동기화</li><li>디버깅과 테스트</li></ul><p>이 중 세 번째 항목인 ‘제어 흐름과 컨텍스트 유실’이 발생하는 이유는, 스택 트레이스가 요청 처리 전체를 아우르는 트랜잭션이 아니라, 그 중 일부만을 처리하는 스레드에 바운드 되므로, 현재 스레드에 할당된 일부 단계에 대한 정보만 스택 트레이스로 확인할 수 있기 때문이다. 그래서 디버깅이나 프로파일링이 어려워질 수 있다.</p><h3 id="코드"><a href="#코드" class="headerlink" title="코드"></a>코드</h3><p>무엇보다도 일단 코드 량이 확연히 늘어난 것을 볼 수 있다. <code>Runnable</code>을 구현해야 하고, 수동으로 스레드를 생성하고 제어하며, 동기화도 필요하다. 로직이 여기저기 흩어져 있어 따라가기가 어렵다.</p><p>반면에 실행 성능은 꽤 좋다. 스레드 생성과 컨텍스트 스위칭 비용이 있으므로 이상적인 수치인 1,300ms에는 못 미치지만 앞서 살펴본 ‘동시성 미사용’ 방식에 비하면 훨씬 좋다.</p><p>아래는 가독성을 위해 일부만 가져왔으며 네이티브 멀티 스레딩 방식 전체 코드는 <a href="https://github.com/bejancsaba/java-concurrency-evolution/blob/main/src/test/java/com/concurrency/evolution/C2_Threads.java" target="_blank" rel="noopener">여기</a>에서 확인할 수 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">shouldExecuteIterationsConcurrently</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line"></span><br><span class="line">    List&lt;Thread&gt; threads = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> user = <span class="number">1</span>; user &lt;= USERS; user++) &#123;</span><br><span class="line">        Thread thread = <span class="keyword">new</span> Thread(<span class="keyword">new</span> UserFlow(user));</span><br><span class="line">        thread.start();</span><br><span class="line">        threads.add(thread);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 종료 조건 - 가장 효율적인 방법은 아니지만 의도대로 동작한다.</span></span><br><span class="line">    <span class="keyword">for</span> (Thread thread : threads) &#123;</span><br><span class="line">        thread.join();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">UserFlow</span> <span class="keyword">implements</span> <span class="title">Runnable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> user;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> List&lt;String&gt; serviceResult = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    UserFlow(<span class="keyword">int</span> user) &#123;</span><br><span class="line">        <span class="keyword">this</span>.user = user;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SneakyThrows</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Thread threadA = <span class="keyword">new</span> Thread(<span class="keyword">new</span> Service(<span class="keyword">this</span>, <span class="string">"A"</span>, SERVICE_A_LATENCY, user));</span><br><span class="line">        Thread threadB = <span class="keyword">new</span> Thread(<span class="keyword">new</span> Service(<span class="keyword">this</span>, <span class="string">"B"</span>, SERVICE_B_LATENCY, user));</span><br><span class="line">        threadA.start();</span><br><span class="line">        threadB.start();</span><br><span class="line">        threadA.join();</span><br><span class="line">        threadB.join();</span><br><span class="line"></span><br><span class="line">        List&lt;Thread&gt; threads = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt;= PERSISTENCE_FORK_FACTOR; i++) &#123;</span><br><span class="line">            Thread thread = <span class="keyword">new</span> Thread(<span class="keyword">new</span> Persistence(i, serviceResult.get(<span class="number">0</span>), serviceResult.get(<span class="number">1</span>)));</span><br><span class="line">            thread.start();</span><br><span class="line">            threads.add(thread);</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment">// 종료 조건 - 가장 효율적인 방법은 아니지만 의도대로 동작한다.</span></span><br><span class="line">        <span class="keyword">for</span> (Thread thread : threads) &#123;</span><br><span class="line">            thread.join();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">addToResult</span><span class="params">(String result)</span> </span>&#123;</span><br><span class="line">        serviceResult.add(result);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Service와 Persistence 구현 코드는 생략</span></span><br></pre></td></tr></table></figure><h3 id="재미있는-사실"><a href="#재미있는-사실" class="headerlink" title="재미있는 사실"></a>재미있는 사실</h3><p>스레드를 무한정 생성할 수는 없다. OS마다 다르며 필자의 64비트 시스템에서 스레드 하나 당 1MB의 메모리(스레드 스택에 사용되는 메모리)를 점유한다. 요청 1000개, 30회 저장으로 설정하고 실행하면 33,000개의 스레드를 생성하려다가 Out Of Memory 에러가 발생한다.</p><p><img src="https://i.imgur.com/Xs06gtv.png" alt="Imgur"></p><h2 id="ExecutorService"><a href="#ExecutorService" class="headerlink" title="ExecutorService"></a>ExecutorService</h2><p>자바 5에 <code>ExecutorService</code>가 도입됐다. 스레드 풀링을 통해 새 스레드 생성 부담을 덜고 스레드를 로우 레벨로 다루는 부담을 덜어내는 게 주된 목표였다. 태스크는 <code>ExecutorService</code>에 submit 되고, 큐에 들어간다. 작업 가능한 스레드가 큐에서 태스크를 가져가서 실행한다.</p><p><img src="https://i.imgur.com/zv9sPMk.jpg" alt="Imgur"></p><p>눈여겨 볼 점은 다음과 같다.</p><ul><li>JVM 스레드 갯수가 여전히 OS 스레드 갯수에 의해 제한을 받는다.</li><li>스레드 풀에서 스레드를 하나 가져가면, 그 스레드는 연산을 수행하지 않더라도 다른 곳에 사용되지 못하고 낭비된다.</li><li><code>Future</code>가 반환되므로 발전된 것 같아 보이지만 조립(compose)할 수 없으며, 반환값을 얻기 위해 <code>get()</code>을 호출하면 태스크가 완료될 때까지 블로킹 된다.</li></ul><h3 id="코드-1"><a href="#코드-1" class="headerlink" title="코드"></a>코드</h3><p>대체로 앞에서 살펴본 ‘네이티브 멀티 스레딩’과 비슷하다. 가장 큰 차이점은 스레드를 직접 생성하지 않고, 태스크를 <code>ExecutorService</code>에 submit 한다는 점이다. 스레드 풀 생성과 관리는 <code>ExecutorService</code>가 담당한다.</p><p>또 다른 점은 서비스 A와 서비스 B를 동기화하기 위해 <code>join()</code>을 사용하지 않는다는 점이다. 대신에 반환되는 <code>Future</code>의 <code>get()</code>을 호출해서 값이 반환될 때까지 블로킹한다.</p><p>예제에서는 2,000개의 스레드를 가진 스레드 풀이 사용됐다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">shouldExecuteIterationsConcurrently</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> user = <span class="number">1</span>; user &lt;= USERS; user++) &#123;</span><br><span class="line">        executor.execute(<span class="keyword">new</span> UserFlow(user));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 종료 조건</span></span><br><span class="line">    latch.await();</span><br><span class="line">    executor.shutdown();</span><br><span class="line">    executor.awaitTermination(<span class="number">60</span>, TimeUnit.SECONDS);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">UserFlow</span> <span class="keyword">implements</span> <span class="title">Runnable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> user;</span><br><span class="line"></span><br><span class="line">    UserFlow(<span class="keyword">int</span> user) &#123;</span><br><span class="line">        <span class="keyword">this</span>.user = user;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@SneakyThrows</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Future&lt;String&gt; serviceA = executor.submit(<span class="keyword">new</span> Service(<span class="string">"A"</span>, SERVICE_A_LATENCY, user));</span><br><span class="line">        Future&lt;String&gt; serviceB = executor.submit(<span class="keyword">new</span> Service(<span class="string">"B"</span>, SERVICE_B_LATENCY, user));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt;= PERSISTENCE_FORK_FACTOR; i++) &#123;</span><br><span class="line">            executor.execute(<span class="keyword">new</span> Persistence(i, serviceA.get(), serviceB.get()));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        latch.countDown();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// Service와 Persistence 구현 코드는 생략</span></span><br></pre></td></tr></table></figure><h3 id="재미있는-사실-1"><a href="#재미있는-사실-1" class="headerlink" title="재미있는 사실"></a>재미있는 사실</h3><p>2천 개의 스레드를 사용했을 때 앞서 살펴본 ‘네이티브 멀티 스레딩’에 비해 훨씬 나은 성능을 보이고 있다.</p><p><img src="https://i.imgur.com/EkgpWg9.png" alt="Imgur"></p><p>스레드 풀 크기를 제대로 설정하지 않으면 데드락이 발생하기도 한다. 풀 크기를 예를 들어 10정도로 작게 잡고 요청 갯수와 저장 횟수를 늘리면 데드락 때문에 아무 것도 제대로 완료되지 않는 것을 볼 수 있다. 왜냐하면 하나의 <code>UserFlow</code>가 다수의 스레드를 필요로 하도록 구현돼 있고, 다수의 <code>UserFlow</code>를 <code>ExecutorService</code>에 submit 하므로 풀에 있는 모든 스레드를 점유하게 되어 태스크를 완료할 수 있는 스레드가 남아있지 않기 때문이다.</p><p>예제에서는 스레드 풀 크기가 고정된 <code>fixedThreadPool</code>을 사용했지만, 크기가 동적으로 변하는 다른 스레드 풀을 사용할 수도 있다.</p><h2 id="Fork-Join-프레임워크"><a href="#Fork-Join-프레임워크" class="headerlink" title="Fork/Join 프레임워크"></a>Fork/Join 프레임워크</h2><p>자바 7에서 <code>ExecutorService</code> 기반으로 만들어진 Fork/Join 프레임워크가 도입됐다. Fork/Join 프레임워크는 재귀적으로 더 작은 크기로 쪼갤 수 있는 태스크를 효율적으로 처리하기 위해 만들어졌다. Fork/Join 프레임워크가 <code>ExecutorService</code>를 대체할 거라는 기대도 있었지만, Fork/Join 프레임워크는 동시 실행에 대해 개발자가 제어할 수 있는 옵션이 더 적으므로 <code>ExecutorService</code>는 여전히 계속 사용되고 있다. </p><p><code>ExecutorService</code>와 확연히 다른 점은 작업 빼가기(work-stealing)다.</p><p><img src="https://i.imgur.com/pVcPHVf.png" alt="Imgur"></p><p>스레드 풀에 있던 스레드 A에 과부하가 걸려서 A 스레드 내부 큐가 꽉 차 있을 때, 스레드 풀에 있는 다른 스레드 B가 <code>ExecutorService</code>의 메인 큐에 있는 태스크를 가져오는 대신에 과부하 걸린 스레드 A 내부 큐에 있는 태스크를 가져와서 처리할 수 있다.</p><h3 id="코드-2"><a href="#코드-2" class="headerlink" title="코드"></a>코드</h3><p>스트림과 람다식 덕분이기도 하지만, 코드가 전체적으로 점점 짧아지고 있다.</p><p>Fork/Join 풀에 태스크를 submit 할 수 있는 <code>UserFlowRecursiveAction</code>를 구현했다. 예제 태스크가 재귀적으로 분할될 성질이 아니기 때문에 이런 태스크에 Fork/Join 프레임워크를 사용하는 것은 사실 적합하지는 않다. 다만 앞서 다뤄온 예제들의 연장선상에서 Fork/Join 프레임워크가 어떻게 동작하는지 알아보자는 목적에는 부합한다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">shouldExecuteIterationsConcurrently</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line"></span><br><span class="line">    commonPool.submit(<span class="keyword">new</span> UserFlowRecursiveAction(IntStream.rangeClosed(<span class="number">1</span>, USERS)</span><br><span class="line">            .boxed()</span><br><span class="line">            .collect(Collectors.toList())));</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Stop Condition</span></span><br><span class="line">    commonPool.shutdown();</span><br><span class="line">    commonPool.awaitTermination(<span class="number">60</span>, TimeUnit.SECONDS);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">UserFlowRecursiveAction</span> <span class="keyword">extends</span> <span class="title">RecursiveAction</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> List&lt;Integer&gt; workload;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">UserFlowRecursiveAction</span><span class="params">(List&lt;Integer&gt; workload)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.workload = workload;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">compute</span><span class="params">()</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (workload.size() &gt; <span class="number">1</span>) &#123;</span><br><span class="line">            commonPool.submit(<span class="keyword">new</span> UserFlowRecursiveAction(workload.subList(<span class="number">1</span>, workload.size())));</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">int</span> user = workload.get(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">        ForkJoinTask&lt;String&gt; taskA = commonPool.submit(() -&gt; service(<span class="string">"A"</span>, SERVICE_A_LATENCY, user));</span><br><span class="line">        ForkJoinTask&lt;String&gt; taskB = commonPool.submit(() -&gt; service(<span class="string">"B"</span>, SERVICE_B_LATENCY, user));</span><br><span class="line"></span><br><span class="line">        IntStream.rangeClosed(<span class="number">1</span>, PERSISTENCE_FORK_FACTOR)</span><br><span class="line">                .forEach(i -&gt; commonPool.submit(() -&gt; persistence(i, taskA.join(), taskB.join())));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="재미있는-사실-2"><a href="#재미있는-사실-2" class="headerlink" title="재미있는 사실"></a>재미있는 사실</h3><p>앞서 ‘ExecutorService’ 단원에서 해봤던 것처럼 이번에도 풀 크기를 10 정도로 잡게 잡고 실행해보자. 이번에는 Fork/Join 프레임워크의 작업 빼가기 기능 덕분에 데드락이 발생하지 완료된다. 하지만 Fork/Join 프레임워크를 사용한다고해서 데드락이 항상 발생하지 않는 것은 아니다. Fork/Join 프레임워크에서는 태스크가 어떤 방식으로 더 작은 태스크로 분할될 수 있는지에 따라 데드락 발생 여부가 정해진다.</p><p>예제 태스크는 재귀적 분할에 딱 들어맞는 태스크가 아니며, Fork/Join 프레임워크에 추가된 로직 때문에 실행 성능은 좋지 않아서 <code>ExecutorService</code> 방식 보다도 오히려 떨어진다. 하지만 작은 풀 사이즈에서도 데드락이 발생하지 않았으므로 안정성은 더 높다.</p><p><img src="https://i.imgur.com/h88yVaX.png" alt="Imgur"></p><h2 id="CompletableFuture"><a href="#CompletableFuture" class="headerlink" title="CompletableFuture"></a>CompletableFuture</h2><p>자바 8에서 도입된 <code>CompletableFuture</code>는 Fork/Join 프레임워크를 기반으로 만들어졌다. 연산 결과를 모아서(combine) 처리할 수 있는 메서드가 하나도 없었고, 에러 처리를 위한 방법도 없었던 <code>Future</code> 인터페이스 도입 이후로 오랫동안 기다려 왔던 진화가 <code>CompletableFuture</code>에서 드디어 이루어졌다.</p><p><code>CompletableFuture</code>를 통해 개선된 점은 다음과 같다.</p><ul><li>더 개선된 함수형 프로그래밍 스타일 도입</li><li>로직을 조립(compose)하고, 결과를 모아서 처리(combine)하고, 비동기 연산 과정을 실행하고, 에러를 처리할 수 있는 50여개의 메서드 추가</li><li><code>CompletableFuture</code>의 평문형 API(fluent API) 대부분은 뒤에 <code>Async</code> 접미사가 붙은 것과 붙지 않은 것, 이렇게 2가지씩 짝지어져 있다. <code>Async</code> 접미사가 붙은 메서드는 해당 연산을 다른 스레드에서 실행하려고 할 때 사용된다.</li></ul><h2 id="코드-3"><a href="#코드-3" class="headerlink" title="코드"></a>코드</h2><p>앞에서 다룬 ‘Fork/Join 프레임워크’보다도 훨씬 더 짧아졌고 압축적이다.</p><p>분량뿐 아니라 코딩 스타일 자체에서도 주목할만한 패러다임 변화가 눈에 띈다. 비동기 실행 결과를 모아서 처리하는 작업이 훨씬 자연스러운 코드로 표현된다.</p><p>하지만 함수형 프로그래밍 스타일에 익숙하지 않은 사람들에게는 대단히 생소해 보일 수도 있으며, 금방 적응하기 어려울 수도 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">shouldExecuteIterationsConcurrently</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException, ExecutionException </span>&#123;</span><br><span class="line">    CompletableFuture.allOf(IntStream.rangeClosed(<span class="number">1</span>, USERS)</span><br><span class="line">            .boxed()</span><br><span class="line">            .map(<span class="keyword">this</span>::userFlow)</span><br><span class="line">            .toArray(CompletableFuture[]::<span class="keyword">new</span>)</span><br><span class="line">    ).get();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SneakyThrows</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> CompletableFuture&lt;String&gt; <span class="title">userFlow</span><span class="params">(<span class="keyword">int</span> user)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> CompletableFuture.supplyAsync(() -&gt; serviceA(user), commonPool)</span><br><span class="line">            .thenCombine(CompletableFuture.supplyAsync(() -&gt; serviceB(user), commonPool), <span class="keyword">this</span>::persist);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SneakyThrows</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> String <span class="title">persist</span><span class="params">(String serviceA, String serviceB)</span> </span>&#123;</span><br><span class="line">    CompletableFuture.allOf(IntStream.rangeClosed(<span class="number">1</span>, PERSISTENCE_FORK_FACTOR)</span><br><span class="line">            .boxed()</span><br><span class="line">            .map(iteration -&gt; CompletableFuture.runAsync(() -&gt; persistence(iteration, serviceA, serviceB), commonPool))</span><br><span class="line">            .toArray(CompletableFuture[]::<span class="keyword">new</span>)</span><br><span class="line">    ).join();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="string">""</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="재미있는-사실-3"><a href="#재미있는-사실-3" class="headerlink" title="재미있는 사실"></a>재미있는 사실</h3><p><code>CompletableFuture</code>가 Fork/Join 프레임워크를 바탕으로 만들어졌음에도 불구하고 실행 성능은 훨씬 좋다.</p><p><img src="https://i.imgur.com/apiXLU8.png" alt="Imgur"></p><p><code>CompletableFuture</code>는 자바에 있는 몇 안 되는 모나드(monad) 중 하나다. 모나드를 알아보려면 이상한 나라의 앨리스에 나오는 토끼굴에 들어가는 모험을 감수해야 하므로 이 글에서는 다루지 않는다.</p><h2 id="Reactive"><a href="#Reactive" class="headerlink" title="Reactive"></a>Reactive</h2><p>리액티브 얘기를 이어가지 전에 먼저 반드시 구분해둬야 할 것이 있다. 리액티브 아키텍처와 리액티브 프로그래밍은 완전히 다르다는 점이다. 이 글에서는 비동기 데이터 스트림을 처리하고 에러 처리와 배압(backpressure)를 확고하게 지원하는 리액티브 프로그래밍만을 다룬다.</p><p><code>CompletableFuture</code>가 진화해서 리액티브 방식이 됐다고 볼 수도 있지만 사실은 물론 그 이상이다. 리액티브 프로그래밍의 주요 목표는 프로그램 구조를 비동기 이벤트 스트림으로 재구성하는 것이며, 스레드 관리는 라이브러리/프레임워크에 위임한다.</p><p>주목할 점은 다음과 같다.</p><ul><li>데이터를 발생시키는 <code>Observable</code>, 데이터를 소비하는 <code>Observer</code>, 스레드를 관리하는 <code>Scheduler</code>의 삼위 일체</li><li>리액티브 방식을 도입하면 프로그램 흐름 전부가 리액티브 방식으로 같이 바뀌어야 한다는 점에서 전염성이 강하다. 일부에 블로킹 코드가 남아 있으면 리액티브의 장점은 전혀 발휘되지 못한다.</li><li>리액티브 구현체도 여러가지가 있다. 처음에는 RxJava가 있었지만 최근에는 스프링의 Reactor가 대세다. 액터 모델을 구현하는 Akka 프레임워크는 RxJava나 Reactor보다 더 급진적인 리액티브 프로그래밍을 적용하고 있다.</li></ul><h3 id="코드-4"><a href="#코드-4" class="headerlink" title="코드"></a>코드</h3><p>리액티브는 이 글에서는 유일하게 자바 언어 자체적으로는 제공되지 않는 솔루션이다. 예제에서는 스프링 Reactor를 사용했지만 RxJava도 크게 다르지 않다.</p><p>명령형 코딩 스타일에만 익숙하다면 이제 리액티브 코드가 아주 생소해 보일 수 있다. 리액티브 코드에 익숙해지려면 단순히 코딩뿐 아니라 테스트와 디버깅에서도 마인드셋을 바꿔야 한다. 특히 리액티브 코드의 테스트와 디버깅은 상당히 어렵지만 충분히 투자해볼만 하다.</p><p>리액티브 코딩 스타일을 마스터하면 지속적으로 발전하는 리액티브 지원 라이브러리의 도움에 힘입어 대단히 성능이 좋은 코드를 효율적으로 작성할 수 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">shouldExecuteIterationsConcurrently</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    Flux.range(<span class="number">1</span>, USERS)</span><br><span class="line">            .flatMap(i -&gt; Mono.defer(() -&gt; userFlow(i)).subscribeOn(Schedulers.parallel()))</span><br><span class="line">            .blockLast();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> Mono&lt;String&gt; <span class="title">userFlow</span><span class="params">(<span class="keyword">int</span> user)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    Mono&lt;String&gt; serviceA = Mono.defer(() -&gt; Mono.just(serviceA(user))).subscribeOn(Schedulers.elastic());</span><br><span class="line">    Mono&lt;String&gt; serviceB = Mono.defer(() -&gt; Mono.just(serviceB(user))).subscribeOn(Schedulers.elastic());</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> serviceA.zipWith(serviceB, (sA, sB) -&gt; Flux.range(<span class="number">1</span>, PERSISTENCE_FORK_FACTOR)</span><br><span class="line">            .flatMap(i -&gt;</span><br><span class="line">                    Mono.defer(() -&gt; Mono.just(persistence(i, sA, sB))).subscribeOn(Schedulers.elastic())</span><br><span class="line">            )</span><br><span class="line">            .blockLast()</span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="재미있는-사실-4"><a href="#재미있는-사실-4" class="headerlink" title="재미있는 사실"></a>재미있는 사실</h3><p>작업 처리를 더 세밀하게 제어할 수 있게 됐지만, 스레드 풀 튜닝은 알아서 수행되므로 개발자가 직접 할 필요는 없다. 예제 코드에서는 리액티브의 진정한 장점 중 하나인 에러 처리나 배압 처리는 사용되지 않았다.</p><p>실행 성능은 <code>CompletableFuture</code>에 비해 떨어진다.</p><p><img src="https://i.imgur.com/UwjyH08.png" alt="Imgur"></p><h2 id="Project-Loom"><a href="#Project-Loom" class="headerlink" title="Project Loom"></a>Project Loom</h2><p>프로젝트 룸(Loom)은 아직 정식 출시되지 않았다. 2020년 12월 현재 기준 자바 16 Early Access 버전에 포함돼서 이것저것 실제로 실험해볼 수는 있지만 자바 16에 포함될지는 미지수다.</p><p>프로젝트 룸은 가상 스레드(Virtual Thread)와 관련된 여러 기능이 포함돼 있다. 그 중에서 ‘가상 스레드’와 ‘구조적 동시성(Structured Concurrency)’만 이 글에서 다룬다.</p><p>가상 스레드를 사용하면 <code>JVM 스레드 : OS 스레드 = 1 : 1</code>라는 오랜 등식이 더이상 성립되지 않는다. 가상 스레드는 기존의 JVM 스레드에 비해 훨씬 가볍고 저렴하다. 구조적 동시성을 도입하면 스레드 라이프타임이 스레드가 사용된 코드 블록과 연관(correlate)돼서 동기화가 훨씬 분명해지고, 우리가 익숙한 명령형 코딩 스타일을 그대로 사용할 수 있다.</p><p>주목해볼 점은 다음과 같다.</p><ul><li>메타데이터, 스택 메모리, 컨텍스트 스위치 시간이 네이티브 OS 스레드의 수 분의 일 밖에 되지 않을만큼 가볍다.</li><li>아직 지원 도구가 충분하지 않다. 프로젝트 룸을 사용할 때 IntelliJ, Gradle, Lombok, JProfiler 등에서 여러가지 이슈가 발생했다. 그래서 프로젝트 룸 예제 코드는 <a href="https://github.com/bejancsaba/java-concurrency-evolution-loom" target="_blank" rel="noopener">별도의 프로젝트</a>로 따로 작성했다.</li></ul><h3 id="코드-5"><a href="#코드-5" class="headerlink" title="코드"></a>코드</h3><p>동시성을 전혀 사용하지 않은 단순하고 쉬운 코드와는 차이가 좀 있지만, 동시성을 사용했던 다른 코드들보다는 훨씬 단순하고 친숙해보인다.</p><p>가상 스레드를 지원하는 새로운 <code>ExecutorService</code> 구현체가 있고, <code>AutoCloseable</code> 인터페이스를 구현하고 있어서 try-with-resource 구문으로 사용해서 안전하게 자원을 열고 닫을 수 있다.</p><p>구조적 동시성이 적용돼 있어서 부모 스레드는 자기가 생성한 모든 자식 스레드의 종료를 try 블록 안에서 기다린다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SneakyThrows</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">startConcurrency</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> (var e = Executors.newVirtualThreadExecutor()) &#123;</span><br><span class="line">        IntStream.rangeClosed(<span class="number">1</span>, USERS).forEach(i -&gt; e.submit(() -&gt; userFlow(i)));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SneakyThrows</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">userFlow</span><span class="params">(<span class="keyword">int</span> user)</span> </span>&#123;</span><br><span class="line">    List&lt;Future&lt;String&gt;&gt; result;</span><br><span class="line">    <span class="keyword">try</span> (var e = Executors.newVirtualThreadExecutor()) &#123;</span><br><span class="line">        result = e.invokeAll(List.of(() -&gt; serviceA(user),() -&gt; serviceB(user)));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    persist(result.get(<span class="number">0</span>).get(), result.get(<span class="number">1</span>).get());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">persist</span><span class="params">(String serviceA, String serviceB)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> (var e = Executors.newVirtualThreadExecutor()) &#123;</span><br><span class="line">        IntStream.rangeClosed(<span class="number">1</span>, PERSISTENCE_FORK_FACTOR)</span><br><span class="line">                .forEach(i -&gt; e.submit(() -&gt; persistence(i, serviceA, serviceB)));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="재미있는-사실-5"><a href="#재미있는-사실-5" class="headerlink" title="재미있는 사실"></a>재미있는 사실</h3><p>JVM 차원에서의 개선이기 때문에 도입되면 많은 레거시 애플리케이션/라이브러리들이 별다른 수정 없이도 성능 개선 효과를 그대로 누릴 수 있다.</p><p>표에서 나타난 것처럼 실제로 몇 개의 OS 스레드를 사용했는지는 알 수 없지만, OS 스레드 갯수가 중요한 것은 아니다.</p><p>중요한 것은 프로젝트 룸을 사용했을 때 예제 실행 총 소요 시간이 이상적인 수치인 1,300ms에 가장 가깝다는 점이다. 요청 갯수나 저장 횟수를 늘리더라도 앞에서 살펴봤던 다른 방식들처럼 뚜렷한 성능 저하를 보이지도 않는다. 따라서 프로젝트 룸의 확장성이 상당히 좋다고 얘기할 수 있다.</p><p><img src="https://i.imgur.com/Lu0h22b.png" alt="Imgur"></p><h2 id="결론"><a href="#결론" class="headerlink" title="결론"></a>결론</h2><p>동시성 처리가 복잡하다는 건 분명한 사실이다. 동시성을 적용하지 않았던 ‘동시성 미사용’ 예제에 비해 ‘네이티브 멀티 스레딩’ 예제 코드는 훨씬 복잡하고 읽기도 어려우며 디버그하기는 거의 불가능했다.</p><p>하지만 동시성 처리 전문가가 되고 싶지 않거나 되고 싶더라도 시간이 없었던 개발자들에게도 마침내 희망의 등불이 켜졌다. 프로젝트 룸 덕분에 동시성 처리 전문가가 되지 않더라도 충분히 성능 좋은 코드를 작성할 수 있게 됐다.</p><p>프로젝트 룸이 출시되면 리액티브 프로그래밍을 완전히 대체할지는 아직 알 수 없다. 만병통치약은 존재하지 않는다는 사실을 감안할 때 프로젝트 룸과 리액티브 프로그래밍은 공존할 가능성이 높다고 본다. 다양한 지원 도구를 갖고 있는 리액티브 프로그래밍으로 해결하는 것이 더 나은 문제도 있을 것이다. 아직까지는 지원 도구가 많지 않지만 자바 동시성 진화 과정에서 더 자연스럽고 더 친숙한 다음 단계는 프로젝트 룸이라고 할 수 있다.</p><hr><p>예제 성능 측정치는 모두 필자의 로컬 장비에서 수행된 결과이므로 과학적인 검증을 거친 측정치라고 볼 수는 없다. 하지만 다양한 방식을 비교하는 목적으로는 의미있는 수치라고 할 수 있다.</p><p>프로젝트 룸 적용 코드는 <a href="https://github.com/bejancsaba/java-concurrency-evolution-loom" target="_blank" rel="noopener">여기</a>에서, 룸을 사용하지 않은 다른 코드는 <a href="https://github.com/bejancsaba/java-concurrency-evolution" target="_blank" rel="noopener">여기</a>에서 확인할 수 있다.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Java-Concurrency-Evolution&quot;&gt;&lt;a href=&quot;#Java-Concurrency-Evolution&quot; class=&quot;headerlink&quot; title=&quot;Java Concurrency Evolution&quot;&gt;&lt;/a&gt;Java Con
      
    
    </summary>
    
      <category term="Language" scheme="http://homoefficio.github.io/categories/Language/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="Reactor" scheme="http://homoefficio.github.io/tags/Reactor/"/>
    
      <category term="Reactive" scheme="http://homoefficio.github.io/tags/Reactive/"/>
    
      <category term="Concurrency" scheme="http://homoefficio.github.io/tags/Concurrency/"/>
    
      <category term="Thread" scheme="http://homoefficio.github.io/tags/Thread/"/>
    
      <category term="MultiThread" scheme="http://homoefficio.github.io/tags/MultiThread/"/>
    
      <category term="ExecutorService" scheme="http://homoefficio.github.io/tags/ExecutorService/"/>
    
      <category term="Fork/Join" scheme="http://homoefficio.github.io/tags/Fork-Join/"/>
    
      <category term="CompletableFuture" scheme="http://homoefficio.github.io/tags/CompletableFuture/"/>
    
      <category term="Project Loom" scheme="http://homoefficio.github.io/tags/Project-Loom/"/>
    
      <category term="Loom" scheme="http://homoefficio.github.io/tags/Loom/"/>
    
      <category term="Virtual Thread" scheme="http://homoefficio.github.io/tags/Virtual-Thread/"/>
    
      <category term="Structured Concurrency" scheme="http://homoefficio.github.io/tags/Structured-Concurrency/"/>
    
  </entry>
  
  <entry>
    <title>무부심 프로그래밍 십계명</title>
    <link href="http://homoefficio.github.io/2020/12/05/%EB%AC%B4%EB%B6%80%EC%8B%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%8B%AD%EA%B3%84%EB%AA%85/"/>
    <id>http://homoefficio.github.io/2020/12/05/무부심-프로그래밍-십계명/</id>
    <published>2020-12-05T04:50:42.000Z</published>
    <updated>2022-03-18T16:07:46.621Z</updated>
    
    <content type="html"><![CDATA[<h1 id="무부심-프로그래밍-십계명"><a href="#무부심-프로그래밍-십계명" class="headerlink" title="무부심 프로그래밍 십계명"></a>무부심 프로그래밍 십계명</h1><p>코딩 호러의 글 <a href="https://blog.codinghorror.com/the-ten-commandments-of-egoless-programming/" target="_blank" rel="noopener">The Ten Commandments of Egoless Programming</a>을 <strong>무부심 프로그래밍 십계명</strong>이라는 이름으로 옮겨봤다.</p><p><strong>무부심</strong>이라는 표현은 법정 스님의 무소유에서 따왔다.<br><strong>무소유는 소유하지 말자가 아니라, 불필요한 소유에서 벗어나자는 것</strong>이다.</p><p>마찬가지로 <strong>자부심을 갖지 말자가 아니라, 불필요하게 지나친 자부심에서 벗어나자는 뜻</strong>에서,<br><strong>Egoless</strong>를 <strong>무부심</strong>으로 옮겼다.</p><h2 id="1-실수한다는-점을-이해하고-받아들여라"><a href="#1-실수한다는-점을-이해하고-받아들여라" class="headerlink" title="1. 실수한다는 점을 이해하고 받아들여라"></a>1. 실수한다는 점을 이해하고 받아들여라</h2><p>실수하지 않을 수는 없다. 실수가 실제 운영 코드에 스며들기 전에 찾아내는 것이 핵심이다.<br>다행스럽게도 극히 일부를 제외한 우리 대부분은 실수하면서 일을 한다.<br>그러니 우리는 실수에서 배우고, 웃고, 털어버리고 한 걸음 더 내디디면 된다.</p><h2 id="2-너-자신과-너의-코드를-구별할-줄-알라"><a href="#2-너-자신과-너의-코드를-구별할-줄-알라" class="headerlink" title="2. 너 자신과 너의 코드를 구별할 줄 알라"></a>2. 너 자신과 너의 코드를 구별할 줄 알라</h2><p>리뷰의 목적은 오직 문제 발견 뿐이다. 그리고 문제가 있다면 발견이 된다.<br>내가 짠 코드에서 문제가 발견된다면 그저 문제가 있는 코드를 발견했을 뿐이다.<br>그러니 내가 짠 코드에 숨어있던 문제를 나 자신에 대한 문제라고 생각하지 말자.<br>이건 내가 아니라 동료에 대해서도 마찬가지다.</p><h2 id="3-고수는-분명히-존재한다"><a href="#3-고수는-분명히-존재한다" class="headerlink" title="3. 고수는 분명히 존재한다"></a>3. 고수는 분명히 존재한다</h2><p>물어보면 고수는 답을 해줄 수 있다. 그러니 다른 사람들에게 묻고 받아들여라.<br>특히 물어볼 필요가 없다고 생각할 때에도 묻고 받아들여라.</p><h2 id="4-의논-없이-문제-없는-코드를-고치지-마라"><a href="#4-의논-없이-문제-없는-코드를-고치지-마라" class="headerlink" title="4. 의논 없이 문제 없는 코드를 고치지 마라"></a>4. 의논 없이 문제 없는 코드를 고치지 마라</h2><p>잘못된 코드를 고치는 것과 잘 실행되는 코드를 다른 스타일로 재작성하는 것은 미세하게 다르다.<br>그 차이를 이해하고, 코드 스타일은 혼자서 강제하지 말고 코드 리뷰를 거친 후에 바꾸자.</p><h2 id="5-잘-모르는-사람도-존중-존경-인내를-갖고-대하라"><a href="#5-잘-모르는-사람도-존중-존경-인내를-갖고-대하라" class="headerlink" title="5. 잘 모르는 사람도 존중, 존경, 인내를 갖고 대하라"></a>5. 잘 모르는 사람도 존중, 존경, 인내를 갖고 대하라</h2><p>개발자와 정기적으로 협업하는 대부분의 비개발자는, 개발자는 좋게 말하면 자기가 무척 잘난 줄 착각하는 사람이고, 나쁘게 말하면 울보 어린아이 같다고 생각한다.<br>참을성 없이 걸핏하면 화를 내면서 이런 고정관념을 고착화하는데 일조하지 말라.</p><h2 id="6-세상에서-변하지-않는-유일한-사실은-변한다는-사실뿐이다"><a href="#6-세상에서-변하지-않는-유일한-사실은-변한다는-사실뿐이다" class="headerlink" title="6. 세상에서 변하지 않는 유일한 사실은, 변한다는 사실뿐이다"></a>6. 세상에서 변하지 않는 유일한 사실은, 변한다는 사실뿐이다</h2><p>변화에 마음을 열고 웃으면서 받아들여라.<br>요구사항, 플랫폼, 도구 등 모든 것은 변하기 마련이다. 변화를 싸워서 물리쳐야할 대상으로 생각하지 말고 새로운 도전이라고 생각하라.</p><h2 id="7-진정한-권위는-지위가-아니라-지식에서-나온다"><a href="#7-진정한-권위는-지위가-아니라-지식에서-나온다" class="headerlink" title="7. 진정한 권위는 지위가 아니라 지식에서 나온다"></a>7. 진정한 권위는 지위가 아니라 지식에서 나온다</h2><p>지식에는 권위가 따라오고, 권위에는 존경이 따라온다. 따라서 무부심 세상에서 존경을 받으려면 지식을 연마하라.</p><h2 id="8-신념을-위해-싸우되-패배도-받아들여라"><a href="#8-신념을-위해-싸우되-패배도-받아들여라" class="headerlink" title="8. 신념을 위해 싸우되 패배도 받아들여라"></a>8. 신념을 위해 싸우되 패배도 받아들여라</h2><p>너의 의견은 거절될 수도 있다. 나중에 너의 의견이 옳았다고 판명되더라도 복수하거나 ‘거봐 내 말대로 했어야지’ 같은 얘기를 여러 번 하지 마라.<br>아깝게 버려진 너의 의견을 순교자인 것처럼 미화하거나 강령으로 내세우지 마라.</p><h2 id="9-골방-개발자가-되지-마라"><a href="#9-골방-개발자가-되지-마라" class="headerlink" title="9. 골방 개발자가 되지 마라"></a>9. 골방 개발자가 되지 마라</h2><p>콜라를 살 때만 방 밖으로 나오는 골방 개발자가 되지 마라. 개방적으로 협력하는 세상에는 연락도 안 되고, 보이지도 않고, 제어할 수도 없는 골방 개발자를 위한 자리는 없다.</p><h2 id="10-사람이-아니라-코드를-비평하라-코드에게-친절하지-말고-코드를-작성한-사람에게-친절하라"><a href="#10-사람이-아니라-코드를-비평하라-코드에게-친절하지-말고-코드를-작성한-사람에게-친절하라" class="headerlink" title="10. 사람이 아니라 코드를 비평하라 - 코드에게 친절하지 말고 코드를 작성한 사람에게 친절하라"></a>10. 사람이 아니라 코드를 비평하라 - 코드에게 친절하지 말고 코드를 작성한 사람에게 친절하라</h2><p>가능한 모든 의견을 긍정적으로 표현하고 코드를 개선하는 방향으로 전개하라. 현재 상황에서 정해진 표준, 규격, 성능 개선 등을 바탕으로 의견을 개진하라.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;무부심-프로그래밍-십계명&quot;&gt;&lt;a href=&quot;#무부심-프로그래밍-십계명&quot; class=&quot;headerlink&quot; title=&quot;무부심 프로그래밍 십계명&quot;&gt;&lt;/a&gt;무부심 프로그래밍 십계명&lt;/h1&gt;&lt;p&gt;코딩 호러의 글 &lt;a href=&quot;https://
      
    
    </summary>
    
      <category term="Philosophy" scheme="http://homoefficio.github.io/categories/Philosophy/"/>
    
      <category term="Psychology" scheme="http://homoefficio.github.io/categories/Philosophy/Psychology/"/>
    
    
      <category term="Egoless" scheme="http://homoefficio.github.io/tags/Egoless/"/>
    
      <category term="Programming" scheme="http://homoefficio.github.io/tags/Programming/"/>
    
      <category term="Code Review" scheme="http://homoefficio.github.io/tags/Code-Review/"/>
    
      <category term="Collaboration" scheme="http://homoefficio.github.io/tags/Collaboration/"/>
    
  </entry>
  
  <entry>
    <title>Constants vs Util</title>
    <link href="http://homoefficio.github.io/2020/12/03/Constants-vs-Util/"/>
    <id>http://homoefficio.github.io/2020/12/03/Constants-vs-Util/</id>
    <published>2020-12-03T08:08:09.000Z</published>
    <updated>2022-03-18T16:07:46.198Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Constants-vs-Util"><a href="#Constants-vs-Util" class="headerlink" title="Constants vs Util"></a>Constants vs Util</h1><p>오늘 일하다 의견을 나누게 된 Constants와 Util 얘기</p><p>애플리케이션이 실행되는 서버 IP를 가져와서 로그에 남길 일이 있다.</p><p>이유는 모르겠지만 <code>InetAddress.getLocalHost().getHostAddress()</code>로 찍으면 127.0.0.1 이 출력돼서 다른 방법을 찾아보니 아래와 같이 <code>NetworkInterface</code> 라는 놈을 써서 구할 수 있었다.</p><p>로직이 복잡해 보이긴 하지만 어쨌든 서버 IP라는 상수를 구하는 로직이라 아래와 같이 Constants 클래스에 private static 메서드로 넣었는데,</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Constants</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 다른 상수 2개 있음</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String SERVER_IP = getServerIp();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> String <span class="title">getServerIp</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Enumeration&lt;NetworkInterface&gt; enis = NetworkInterface.getNetworkInterfaces();</span><br><span class="line">            List&lt;NetworkInterface&gt; nis = Collections.list(enis);</span><br><span class="line">            <span class="keyword">for</span> (NetworkInterface ni : nis) &#123;</span><br><span class="line">                <span class="keyword">if</span> (ni.isUp() &amp;&amp; !ni.isLoopback() &amp;&amp; ni.getHardwareAddress() != <span class="keyword">null</span>) &#123;</span><br><span class="line">                    List&lt;InterfaceAddress&gt; addresses = ni.getInterfaceAddresses();</span><br><span class="line">                    <span class="keyword">for</span> (InterfaceAddress address : addresses) &#123;</span><br><span class="line">                        <span class="keyword">if</span> (!StringUtils.isEmpty(address.toString())) &#123;</span><br><span class="line">                            <span class="keyword">return</span> address.getAddress().getHostAddress();</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (SocketException e) &#123;</span><br><span class="line">            <span class="comment">// 없으면 공백 반환 처리</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">""</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>동료가 Constants 에는 메서드가 있으면 Constants 클래스 성격에 안 맞는다며 <code>getServerIp()</code>를 외부 Util 클래스로 빼자고 한다.</p><p>엉? 아니 Constants 클래스에 메서드가 있으면 안 되는 건가? 다른 메서드도 아니고 상수를 구하는 데 사용되는 private static 메서드인데?</p><p>나는 이 메서드는 분명히 SERVER_IP 라는 상수를 구하는 책임을 가지고 있고, 외부에서 호출할 필요가 없으므로 외부로 뺄 게 아니라 private static 으로 Constants 에 두는 게 적절하다고 봤다. 이게 정보와 로직을 함께 모아 넣자는 캡슐화의 원리에 부합하기 때문이다.</p><p>물론 상수가 많아지면 상황에 맞게 클래스를 분리해서 비대화를 막아야겠지만, 지금은 비대화와는 거리가 멀어 보이고, 분리한다고 해도 메서드의 존재 여부가 분리의 기준일 리는 없다고 생각한다.</p><p>하지만 동료는 Constants 라는 이름의 클래스는 통상적으로 상수만 모아 놓는 목적으로 만드는 거라, 이 클래스에 메서드를 넣으면 그 목적을 잃는다고 한다. 다른 동료도 이 메서드는 Util 클래스로 빼는 게 좋다고 한다.</p><p>아 그래? 메서드를 쓰면 안 되는 건가? 하고 다음과 같이 복잡해보이지만 메서드 호출이 아닌 단순 할당식으로 바꿔서 반응을 살펴봤다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String SERVER_IP = ((Supplier&lt;String&gt;) () -&gt; &#123;</span><br><span class="line">     <span class="keyword">try</span> &#123;</span><br><span class="line">         Enumeration&lt;NetworkInterface&gt; enis = NetworkInterface.getNetworkInterfaces();</span><br><span class="line">         List&lt;NetworkInterface&gt; nis = Collections.list(enis);</span><br><span class="line">         <span class="keyword">for</span> (NetworkInterface ni : nis) &#123;</span><br><span class="line">             <span class="keyword">if</span> (ni.isUp() &amp;&amp; !ni.isLoopback() &amp;&amp; ni.getHardwareAddress() != <span class="keyword">null</span>) &#123;</span><br><span class="line">                 List&lt;InterfaceAddress&gt; addresses = ni.getInterfaceAddresses();</span><br><span class="line">                 <span class="keyword">for</span> (InterfaceAddress address : addresses) &#123;</span><br><span class="line">                     <span class="keyword">if</span> (!StringUtils.isEmpty(address.toString())) &#123;</span><br><span class="line">                         <span class="keyword">return</span> address.getAddress().getHostAddress();</span><br><span class="line">                     &#125;</span><br><span class="line">                 &#125;</span><br><span class="line">             &#125;</span><br><span class="line">         &#125;</span><br><span class="line">     &#125; <span class="keyword">catch</span> (SocketException e) &#123;</span><br><span class="line">         <span class="comment">// 없으면 공백 반환 처리</span></span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">return</span> <span class="string">""</span>;</span><br><span class="line"> &#125;).get();</span><br></pre></td></tr></table></figure><p>예상한대로 여전히 복잡한 로직이 있으니 외부 Util 로 빼는 게 좋다고 한다.</p><p>그러니까 결국 메서드라는 형식이 문제가 아니라 상수를 구하는 로직이 복잡하다면 외부로 빼야한다는 얘기였던 거다. 요는 <strong>Constants 클래스에는 복잡한 로직을 두지 말고 오직 간단하고 단순한 상수만 넣자는 얘기다.</strong></p><p>정리하면 다음과 같다.</p><blockquote><p>SERVER_IP라는 똑같은 상수일지라도 그 값을 구하는 데,</p><ul><li>로직이 동원된다면 <code>Constants.SERVER_IP</code>로 하지 말고, <code>ServerUtil.SERVER_IP</code> 로 해야 되고,  </li><li>로직이 동원되지 않는다면 <code>ServerUtil.SERVER_IP</code>로 하지 말고, <code>Constants.SERVER_IP</code>로 해야 된다는 얘기</li></ul></blockquote><p>Util 이나 Constants 나 엎어치나 메치나 좌측 궁뎅이나 우측 방뎅이나 그게 그거 같긴 하지만,<br>설계라는 게 원래 책임, 역할, 이름 이런 거 고민하는 거니께, 탭이나 스페이스 보다는 유익한 얘기 아닐까?<br>(헉 실수다 감히 신성한 탭/스페이스를 운운하다니..=3=3)<br>여러분의 생각은?</p><hr><p>참고로 실무적으로는 다른 동료의 의견에 따라 다음과 같이 마무리했다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String SERVER_IP = System.getProperty(<span class="string">"java.rmi.server.hostname"</span>, <span class="string">""</span>);</span><br></pre></td></tr></table></figure><p><code>System.getProperty()</code>라는 외부 클래스의 public static 메서드 호출은 허용되지만,<br>내부에 private static 메서드를 두고 호출하는 건 안 된다는 얘기다.</p><p>이 정도면 Constants 클래스에는 <strong>시각적으로</strong> 길어 보이는 로직을 두지 말자는 얘기이고,<br>이 지점부터는 설계나 구현 원리/원칙보다는 취향에 가까운 얘기라서,<br>더 이상 얘기를 나누면 생산적이지 않은 쓸모 없는 얘기로 흐를 가능성이 높으니 이 정도에서 멈췄다.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Constants-vs-Util&quot;&gt;&lt;a href=&quot;#Constants-vs-Util&quot; class=&quot;headerlink&quot; title=&quot;Constants vs Util&quot;&gt;&lt;/a&gt;Constants vs Util&lt;/h1&gt;&lt;p&gt;오늘 일하다 의견을
      
    
    </summary>
    
      <category term="Design" scheme="http://homoefficio.github.io/categories/Design/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="Constants" scheme="http://homoefficio.github.io/tags/Constants/"/>
    
      <category term="Util" scheme="http://homoefficio.github.io/tags/Util/"/>
    
      <category term="Design" scheme="http://homoefficio.github.io/tags/Design/"/>
    
  </entry>
  
  <entry>
    <title>Back to the Essence - Java-Servers - (2)</title>
    <link href="http://homoefficio.github.io/2020/11/02/Back-to-the-Essence-Java-Servers-2/"/>
    <id>http://homoefficio.github.io/2020/11/02/Back-to-the-Essence-Java-Servers-2/</id>
    <published>2020-11-01T16:55:14.000Z</published>
    <updated>2022-03-18T16:07:46.127Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Back-to-the-Essence-Java-Servers-2편"><a href="#Back-to-the-Essence-Java-Servers-2편" class="headerlink" title="Back to the Essence - Java Servers - 2편"></a>Back to the Essence - Java Servers - 2편</h1><p><a href="https://homoefficio.github.io/2020/11/02/Back-to-the-Essence-Java-Servers-1/">1편</a>에서 블로킹 방식의 싱글 스레드 소켓 서버를 만들어봤고 다음의 문제가 있음을 발견했다.</p><blockquote><ul><li>블로킹 방식의 싱글 스레드 소켓 서버는 시간 끄는 이상한 클라이언트가 하나만 들어와도 서버가 먹통이 되고, 다른 클라이언트까지 먹통될 수 있다.</li></ul></blockquote><p>이제 시간 끄는 이상한 클라이언트가 들어오더라도 서버나 다른 클라이언트가 먹통이 되지 않도록 개선해야 한다.</p><p>가장 간단한 방법은 클라이언트의 요청마다 별개의 스레드에서 처리하게 하는 것이다. 가장 널리 사용하는 방식이며 서블릿도 이 방식에 기반을 두고 있고, 서블릿에 기반을 둔 Spring MVC도 이 방식이다.</p><h1 id="Classic-IO-BIO-Multi-Thread-ServerSocket"><a href="#Classic-IO-BIO-Multi-Thread-ServerSocket" class="headerlink" title="Classic IO(BIO) - Multi Thread ServerSocket"></a>Classic IO(BIO) - Multi Thread ServerSocket</h1><p>서버에서 멀티 스레드를 사용한다면 크게 두 가지 전략이 떠오른다.</p><ol><li>멀티 스레드로 <code>ServerSocket</code>을 여러 개 띄우고, 이 스레드를 요청 처리 완료때까지 사용한다.</li><li><code>ServerSocket</code>은 하나의 스레드로 하되, <code>ServerSocket.accept()</code>로 받아온 여러 소켓들을 여러 개의 스레드로 처리한다.</li></ol><p>일단 1번은 불가다. 아래와 같은 코드는 두 번째 <code>ServerSocket</code> 생성 시</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ServerSocket serverSocket1 = <span class="keyword">new</span> ServerSocket(Constants.SERVER_PORT);</span><br><span class="line">ServerSocket serverSocket2 = <span class="keyword">new</span> ServerSocket(Constants.SERVER_PORT);</span><br></pre></td></tr></table></figure><p>다음과 같이 <code>BindException</code>이 발생한다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Exception in thread &quot;main&quot; java.net.BindException: Address already in use</span><br></pre></td></tr></table></figure><p>따라서 사용할 수 있는 전략은 2번 뿐이다.</p><p><code>ServerSocket</code>을 사용해서 서버 데몬을 만들고, <code>accept()</code>로 클라이언트의 연결 요청을 기다리는 것까지는 싱글 스레드 서버와 동일하다. 다만 들어온 연결 요청의 처리를 요청 마다 다른 스레드에서 담당한다는 것만 다르다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EchoSocketServerMultiThread</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        EchoSocketServerMultiThread echoSocketServerMultiThread = <span class="keyword">new</span> EchoSocketServerMultiThread();</span><br><span class="line">        echoSocketServerMultiThread.start();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="comment">// 50개짜리 스레드 풀</span></span><br><span class="line">        ExecutorService es = Utils.getCommonExecutorService(<span class="number">50</span>);</span><br><span class="line">        <span class="keyword">try</span> (ServerSocket serverSocket = <span class="keyword">new</span> ServerSocket(Constants.SERVER_PORT);</span><br><span class="line">             FileOutputStream fos = Utils.getCommonFileOutputStream()</span><br><span class="line">        ) &#123;</span><br><span class="line">            Utils.serverTimeStamp(<span class="string">"==============================="</span>, fos);</span><br><span class="line">            Utils.serverTimeStamp(<span class="string">"Multi Thread Socket Echo Server 시작"</span>, fos);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"---------------------------"</span>, fos);</span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"Echo Server 대기 중"</span>, fos);</span><br><span class="line"></span><br><span class="line">                <span class="comment">// accept() 는 연결 요청이 올 때까지 return 하지 않고 blocking</span></span><br><span class="line">                Socket acceptedSocket = serverSocket.accept();</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 연결 요청이 오면 새 thread 에서 요청 처리 로직 수행</span></span><br><span class="line">                es.execute(() -&gt; &#123;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        Utils.serverTimeStamp(<span class="string">"Client 접속!!!"</span>, fos);</span><br><span class="line">                        Utils.serverTimeStamp(<span class="string">"Echo 시작"</span>, fos);</span><br><span class="line"><span class="comment">//                    Utils.sleep(500L);</span></span><br><span class="line">                        EchoProcessor.echo(acceptedSocket);</span><br><span class="line">                        Utils.serverTimeStamp(<span class="string">"Echo 완료"</span>, fos);</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Executors</code>를 이용해서 단순한 고정 크기 스레드 풀을 만들어 사용한다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">Utils</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ExecutorService <span class="title">getCommonExecutorService</span><span class="params">(<span class="keyword">int</span> nThreads)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Executors.newFixedThreadPool(nThreads);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="실습"><a href="#실습" class="headerlink" title="실습"></a>실습</h1><h2 id="금수저-서버-금수저-스레드-풀"><a href="#금수저-서버-금수저-스레드-풀" class="headerlink" title="금수저 서버 + 금수저 스레드 풀"></a>금수저 서버 + 금수저 스레드 풀</h2><p>50개 짜리 스레드 풀을 사용하면서 1편에서 문제를 유발했던 시나리오 그대로 수행해보자.</p><ol><li>EchoSocketServerMultiThread 실행</li><li>EchoSocketClient 실행, EchoSocketClient는 연결 요청 후 5초 후에 메시지를 서버에 전송</li><li>5초 이내에 다른 터미널에서 <code>echo -n &#39;아무거나&#39; | nc localhost 7777</code> 실행해서 메아리가 터미널에 바로 찍히면 문제 해결</li></ol><p>실제로는 실습 편의를 위해 EchoSocketClient가 5초가 아니라 1분 후에 보내도록 설정했다.<br>결과는 아래 움짤과 같이 여러 터미널 창에서 동시에 <code>echo -n &#39;아무거나&#39; | nc localhost 7777</code>를 실행해도 모두 거의 동시에 메아리가 출력됐다.</p><p><img src="https://i.imgur.com/HHYMsq0.gif" alt="Imgur"></p><p>움짤 파일 크기를 작게하기 위해 4개의 터미널창만 캡쳐했지만 실제로는 12개의 창에서 동시에 요청을 날렸다. main 스레드 + EchoSocketClient 요청 처리 스레드 + 12개의 터미널 요청 처리 스레드, 총 14개의 스레드가 사용됐다.</p><p>EchoSocketClient 요청은 pool-1-thread-1 스레드가 담당했고, 12개의 터미널 요청은 pool-1-thread-2 ~ pool-1-thread-13 스레드가 각각 처리했다. 로그를 보면 쉽게 알 수 있다. <code>&lt;==</code>로 표시한 부분은 설명이다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line">scratchpad-server git:main 🍺🦑🍺🍕🍺 ❯ tail -f temp.log                                                                                               ✹</span><br><span class="line">[SERVER -            main] 2020-11-02T01:36:54.427095 - ===============================</span><br><span class="line">[SERVER -            main] 2020-11-02T01:36:54.446306 - Multi Thread Socket Echo Server 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:36:54.446730 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T01:36:54.447015 - Echo Server 대기 중</span><br><span class="line"></span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:00.181527 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-1] 2020-11-02T01:37:00.181621 - Client 접속!!! &lt;== 1분 후 메시지 보내는 EchoSocketClient 요청 처리 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:00.181954 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-1] 2020-11-02T01:37:00.181981 - Echo 시작</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:37:00.197423 - Client 시작</span><br><span class="line"></span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.412302 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T01:37:08.412373 - Client 접속!!! &lt;== 여기서부터는 터미널 nc 클라이언트 요청 처리 시작</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T01:37:08.412695 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.412671 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.413172 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T01:37:08.413241 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T01:37:08.413521 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.413473 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.414077 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T01:37:08.414172 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T01:37:08.414427 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.414355 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.414899 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-5] 2020-11-02T01:37:08.415059 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.415181 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-5] 2020-11-02T01:37:08.415326 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.415654 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-6] 2020-11-02T01:37:08.415793 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.415901 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-6] 2020-11-02T01:37:08.416132 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.416741 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-7] 2020-11-02T01:37:08.417166 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.417073 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-7] 2020-11-02T01:37:08.417448 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-8] 2020-11-02T01:37:08.417732 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-8] 2020-11-02T01:37:08.418018 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.417644 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.418353 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.418818 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-9] 2020-11-02T01:37:08.418987 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.419078 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-9] 2020-11-02T01:37:08.419201 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.419664 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-10] 2020-11-02T01:37:08.419741 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.419985 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-10] 2020-11-02T01:37:08.419997 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.420454 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-11] 2020-11-02T01:37:08.421378 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.421536 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-11] 2020-11-02T01:37:08.421598 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.421942 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-12] 2020-11-02T01:37:08.422346 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.422542 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-12] 2020-11-02T01:37:08.422558 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.423231 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-13] 2020-11-02T01:37:08.423331 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T01:37:08.423519 - Echo Server 대기 중</span><br><span class="line">[SERVER - pool-1-thread-13] 2020-11-02T01:37:08.423555 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-12] 2020-11-02T01:37:08.440014 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-7] 2020-11-02T01:37:08.440467 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T01:37:08.440014 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T01:37:08.440820 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-13] 2020-11-02T01:37:08.441453 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-11] 2020-11-02T01:37:08.441606 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-9] 2020-11-02T01:37:08.442308 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T01:37:08.442538 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-6] 2020-11-02T01:37:08.443030 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-10] 2020-11-02T01:37:08.442771 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-8] 2020-11-02T01:37:08.443385 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-5] 2020-11-02T01:37:08.443648 - Echo 완료</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:38:00.227402 - 메시지 전송 시작 &lt;== EchoSocketClient 가 1분 후 메시지 전송</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:38:00.238871 - 메시지 print 완료</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:38:00.242828 - 메시지 flush 완료</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:38:00.243060 - 서버 Echo 대기...</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:38:00.261370 - 서버 Echo 도착</span><br><span class="line">[SERVER - pool-1-thread-1] 2020-11-02T01:38:00.261379 - Echo 완료 &lt;== pool-1-thread-1은 클라이언트에 의해 1분 동안 블록돼 있는 동안 0.5초 지나가므로 클라이언트로부터 메시지 받자마자 Echo</span><br><span class="line">[CLIENT -            main] 2020-11-02T01:38:00.263653 - 서버 Echo msg: Server Echo - 안녕, echo server</span><br></pre></td></tr></table></figure><h2 id="흑수저-서버-흑수저-스레드-풀"><a href="#흑수저-서버-흑수저-스레드-풀" class="headerlink" title="흑수저 서버 + 흑수저 스레드 풀"></a>흑수저 서버 + 흑수저 스레드 풀</h2><p>이번에는 메아리 처리에 0.5초가 걸리는 후진 서버에서 스레드도 4개만 사용해서 테스트해보자.</p><p>메아리 처리에 0.5초가 걸리도록 EchoProcessor에서 주석 처리 돼 있던 <code>sleep()</code>의 주석을 해제한다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">EchoProcessor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> FileOutputStream fos = Utils.getCommonFileOutputStream();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">echo</span><span class="params">(Socket socket)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> (BufferedReader in = <span class="keyword">new</span> BufferedReader(<span class="keyword">new</span> InputStreamReader(socket.getInputStream()));</span><br><span class="line">             PrintWriter out = <span class="keyword">new</span> PrintWriter(socket.getOutputStream())</span><br><span class="line">        ) &#123;</span><br><span class="line">            String clientMessage = in.readLine();  <span class="comment">// in에 읽을 게 들어올 때까지 blocking</span></span><br><span class="line">            String serverMessage = <span class="string">"Server Echo - "</span> + clientMessage + System.lineSeparator();</span><br><span class="line">            Utils.sleep(<span class="number">500L</span>);  <span class="comment">// Echo 처리에 0.5초가 걸리도록 주석 해제</span></span><br><span class="line">            out.println(serverMessage);</span><br><span class="line">            out.flush();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>EchoSocketServerMultiThread의 스레드 풀을 스레드 4개만 사용하도록 <code>ExecutorService es = Utils.getCommonExecutorService(4);</code>로 바꿔서 흑수저 스레드 풀을 만든다. 그리고 앞에서와 마찬가지로 테스트 해보면 아래 움짤처럼 터미널에 표시되는 메아리에 시차가 조금 발생하는 것을 확인할 수 있다.</p><p><img src="https://i.imgur.com/zhsVRRw.gif" alt="Imgur"></p><p><strong>요청 처리엔 시간이 필요하고, 스레드도 메모리 및 Context Switching 부담이 있어서 마냥 늘릴 수만은 없으므로, 이 흑수저 서버 + 흑수저 스레드 풀 시나리오가 현실에 존재하는 시나리오에 가깝다</strong>고 할 수 있다.</p><p>1분 지연 Java Socket Client 요청 1개, 12개의 터미널 nc 요청 처리 로그는 다음과 같다. 자세한 내용은 <code>&lt;==</code>로 설명을 추가했다.<br>스레드 이름과 화살표 길이, 중간중간 0.5초 정도 차이나는 부분을 유의해서 살펴보면 이해하는 데 도움이 될 것이다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line">scratchpad-server git:main 🍺🦑🍺🍕🍺 ❯ tail -f temp.log        </span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:29.653111 - ===============================</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:29.675600 - Multi Thread Socket Echo Server 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:29.676223 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:29.676571 - Echo Server 대기 중</span><br><span class="line"></span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:36.000378 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-1] 2020-11-02T12:29:36.000475 - Client 접속!!! &lt;== 1분 후 메시지 보내는 EchoSocketClient 요청</span><br><span class="line">[SERVER - pool-1-thread-1] 2020-11-02T12:29:36.000856 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:36.000830 - Echo Server 대기 중</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:29:36.015713 - Client 시작</span><br><span class="line">    </span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.072899 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:43.072977 - Client 접속!!! &lt;== 터미널 요청 1 처리: 2번 스레드에서 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.073253 - Echo Server 대기 중 &lt;== main 스레드는 블록되지 않고 계속 요청을 받아 스레드 풀에 전달하고 다음 요청 대기</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:43.073283 - Echo 시작 &lt;== 요청 1 Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:43.073725 - Client 접속!!! &lt;==== 터미널 요청 2 처리: 3번 스레드에서 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.073657 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:43.073938 - Echo 시작 &lt;==== 요청 2 Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.073984 - Echo Server 대기 중 &lt;== main 스레드는 블록되지 않고 계속 요청을 받아 스레드 풀에 전달하고 다음 요청 대기</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.074461 - ---------------------------</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:43.074550 - Client 접속!!! &lt;====== 터미널 요청 3 처리: 4번 스레드에서 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.074694 - Echo Server 대기 중 &lt;== main 스레드는 블록되지 않고 계속 요청을 받아 스레드 풀에 전달하고 다음 요청 대기</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:43.074716 - Echo 시작 &lt;====== 요청 3 Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.075061 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.075294 - Echo Server 대기 중 &lt;== main 스레드는 블록되지 않고 계속 요청을 받아 스레드 풀에 전달하고 다음 요청 대기</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.075582 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.075844 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.076123 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.076339 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.076611 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.076860 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.080305 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.080641 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.080866 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.081025 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.081308 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.081528 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.081733 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.081907 - Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.082124 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T12:29:43.082250 - Echo Server 대기 중 &lt;== main 스레드는 블록되지 않고 계속 요청을 받아 스레드 풀에 전달하고 다음 요청 대기</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:43.593116 - Echo 완료 &lt;== 요청 1 Echo 완료, 0.5초 소요</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:43.593116 - Echo 완료 &lt;====== 요청 3 Echo 완료, 0.5초 소요</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:43.593413 - Echo 완료 &lt;==== 요청 2 Echo 완료, 0.5초 소요</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:43.593569 - Client 접속!!! &lt;== 요청 1 처리 완료 후 큐에 있던 후속 요청 A: 2번 스레드에서 처리 시작</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:43.593575 - Client 접속!!! &lt;====== 요청 3 처리 완료 후 큐에 있던 후속 요청 C: 4번 스레드에서 처리 시작</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:43.593817 - Echo 시작 &lt;== 요청 A</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:43.593683 - Client 접속!!! &lt;==== 요청 2 처리 완료 후 큐에 있던 후속 요청 B: 3번 스레드에서 처리 시작</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:43.593889 - Echo 시작 &lt;====== 요청 C</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:43.594157 - Echo 시작 &lt;==== 요청 B</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:44.097963 - Echo 완료 &lt;== 요청 A</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:44.097963 - Echo 완료 &lt;====== 요청 C</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:44.098370 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:44.098411 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:44.098672 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:44.098608 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:44.097963 - Echo 완료 &lt;==== 요청 B</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:44.099086 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:44.099283 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:44.603912 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:44.604235 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:44.604316 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:44.604478 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:44.604473 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:44.604637 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:44.604910 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:44.604962 - Client 접속!!!</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:44.605142 - Echo 시작</span><br><span class="line">[SERVER - pool-1-thread-3] 2020-11-02T12:29:45.107464 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-4] 2020-11-02T12:29:45.107464 - Echo 완료</span><br><span class="line">[SERVER - pool-1-thread-2] 2020-11-02T12:29:45.107464 - Echo 완료</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:30:36.051155 - 메시지 전송 시작 &lt;== 1분 후 메시지 보내는 Java Socket Client</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:30:36.068386 - 메시지 print 완료</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:30:36.073274 - 메시지 flush 완료 &lt;== 메시지 전송</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:30:36.073655 - 서버 Echo 대기...</span><br><span class="line">[SERVER - pool-1-thread-1] 2020-11-02T12:30:36.581245 - Echo 완료 &lt;== 메시지 전송 받은 후 0.5초 후에 Echo 완료</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:30:36.582077 - 서버 Echo 도착</span><br><span class="line">[CLIENT -            main] 2020-11-02T12:30:36.585161 - 서버 Echo msg: Server Echo - 안녕, echo server</span><br></pre></td></tr></table></figure><p>요는 다음과 같다.</p><ul><li><code>accept()</code>는 요청이 오는 족족 스레드 풀에 전달<ul><li>요청을 받을 때까지는 블로킹이지만, 요청을 받은 후에는 블로킹하지 않고 바로 다음 요청을 블로킹하면서 대기</li></ul></li><li>스레드 풀은<ul><li>사용 가능한 스레드가 있으면 요청을 바로 처리</li><li>사용 가능한 스레드가 없으면 요청을 큐에 저장 후 나중에 처리</li></ul></li></ul><h1 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h1><blockquote><ul><li><code>ServerSocket.accept()</code>로 클라이언트의 요청을 받아들이는 건 하나의 스레드에서 담당하고,</li><li>요청의 처리는 요청마다 별도의 스레드에서 처리하게 하면,</li><li><p>시간을 오래 끄는 클라이언트가 일부 있더라도 서버와 나머지 클라이언트는 먹통이 발생하지 않는다.</p></li><li><p>요청을 블로킹 방식으로 처리하는 상황에서</p><ul><li>요청 처리에 일정 시간이 필요하고, 스레드가 충분하지 못하다면, 일부 요청은 큐에서 쌓여 대기하다가 시차를 두고 처리된다.</li><li>큐에 쌓여 대기하는 요청을 줄이려면 스레드를 늘리는 수 밖에 없다.</li></ul></li></ul></blockquote><p>결국 제한된 자원 상황에서 많은 요청을 처리하려면 스레드를 늘리는 수 밖에 없지만, 자체 스택을 갖고 있는 스레드를 많이 사용하면 메모리 한계에 부딪힐 수 있고, 많은 스레드는 많은 Context Switching 을 유발해서 성능 저하의 원인이 되기도 한다.</p><p>그렇다면 스레드를 무작정 늘리기보다는 스레드 사용 효율을 높이는 방법을 찾아봐야 할 것 같다.</p><p>3편에서는 스레드 효율을 높일 수 있는 NIO에 대해 알아본다.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Back-to-the-Essence-Java-Servers-2편&quot;&gt;&lt;a href=&quot;#Back-to-the-Essence-Java-Servers-2편&quot; class=&quot;headerlink&quot; title=&quot;Back to the Essence - 
      
    
    </summary>
    
      <category term="Network" scheme="http://homoefficio.github.io/categories/Network/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="I/O" scheme="http://homoefficio.github.io/tags/I-O/"/>
    
      <category term="Java IO" scheme="http://homoefficio.github.io/tags/Java-IO/"/>
    
      <category term="Echo Server" scheme="http://homoefficio.github.io/tags/Echo-Server/"/>
    
      <category term="ServerSocket" scheme="http://homoefficio.github.io/tags/ServerSocket/"/>
    
      <category term="Socket" scheme="http://homoefficio.github.io/tags/Socket/"/>
    
      <category term="Blocking" scheme="http://homoefficio.github.io/tags/Blocking/"/>
    
      <category term="netcat" scheme="http://homoefficio.github.io/tags/netcat/"/>
    
      <category term="accept" scheme="http://homoefficio.github.io/tags/accept/"/>
    
      <category term="Multi Thread" scheme="http://homoefficio.github.io/tags/Multi-Thread/"/>
    
      <category term="Servlet" scheme="http://homoefficio.github.io/tags/Servlet/"/>
    
      <category term="Spring MVC" scheme="http://homoefficio.github.io/tags/Spring-MVC/"/>
    
  </entry>
  
  <entry>
    <title>Back to the Essence - Java Servers - (1)</title>
    <link href="http://homoefficio.github.io/2020/11/02/Back-to-the-Essence-Java-Servers-1/"/>
    <id>http://homoefficio.github.io/2020/11/02/Back-to-the-Essence-Java-Servers-1/</id>
    <published>2020-11-01T15:34:50.000Z</published>
    <updated>2022-03-18T16:07:46.116Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Back-to-the-Essence-Java-Servers-1편"><a href="#Back-to-the-Essence-Java-Servers-1편" class="headerlink" title="Back to the Essence - Java Servers - 1편"></a>Back to the Essence - Java Servers - 1편</h1><p>서버 프로그래밍을 한다고는 하지만, 지난 수년 간 굴러도 스프링 위에서만 구르다보니 스프링 없이는, 아니 이제는 스프링만으로도 뭘 못할 것 같고 스프링 부트 없이는 간단한 메아리(Echo) 서버조차 못 만드는 <del>경지</del>지경에 이르렀다. 이 아니 부끄러운가..</p><p>그래서 Java가 제공해주는 classic IO, NIO, NIO2로 간단한 Echo Server를 만들어보면서 기본기를 좀 다져보려 한다.<br>만드는 데서 그치지 않고 그동안 간접 경험으로만 알아왔던 NIO, NIO2 의 장단점을 부하테스트를 통해 확인해보고자 한다.<br>나름 원대한 계획이지만 목표한 걸 모두 얻을 수 있을지는 미지수다. 그냥 달려보자.</p><h1 id="Client"><a href="#Client" class="headerlink" title="Client"></a>Client</h1><p>서버를 호출할 클라이언트는 크게 3가지다.</p><ul><li>Java Socket Client</li><li>nc(netcat)</li><li>JMeter Client</li></ul><p>이 중에서 코딩이 필요한 건 Java Socket Client 뿐이고 코드는 다음과 같다. 이해를 위해 로깅을 많이 넣었는데, 로깅 빼면 설명할 것도 없다.<br>참고로 로깅을 콘솔이 아닌 temp.log 파일에 찍는다. 이유는 서버와 클라이언트의 로그를 한 군데 모아서 보는 게 이해하는 데 도움이 되기 때문이다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> io.homo_efficio.server.socket;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> io.homo_efficio.server.common.Constants;</span><br><span class="line"><span class="keyword">import</span> io.homo_efficio.server.common.Utils;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.*;</span><br><span class="line"><span class="keyword">import</span> java.net.Socket;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> homo.efficio@gmail.com</span></span><br><span class="line"><span class="comment"> * created on 2020-10-10</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EchoSocketClient</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        String message = <span class="string">"안녕, echo server"</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> (Socket clientSocket = <span class="keyword">new</span> Socket(Constants.SERVER_HOST_NAME, Constants.SERVER_PORT);</span><br><span class="line">             FileOutputStream fos = Utils.getCommonFileOutputStream();</span><br><span class="line">             PrintWriter out = <span class="keyword">new</span> PrintWriter(clientSocket.getOutputStream());</span><br><span class="line">             BufferedReader in = <span class="keyword">new</span> BufferedReader(<span class="keyword">new</span> InputStreamReader(clientSocket.getInputStream()))</span><br><span class="line">        ) &#123;</span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"Client 시작"</span>, fos);</span><br><span class="line">            <span class="comment">// Utils.sleep(5000L);  // 서버 blocking 확인 시 사용</span></span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"메시지 전송 시작"</span>, fos);</span><br><span class="line">            out.println(message);</span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"메시지 print 완료"</span>, fos);</span><br><span class="line">            out.flush();</span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"메시지 flush 완료"</span>, fos);</span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"서버 Echo 대기..."</span>, fos);</span><br><span class="line">            <span class="comment">// in.readLine() 은 읽을 데이터가 들어올 때까지 blocking 이므로 while (true) 불필요</span></span><br><span class="line">            String messageFromServer = in.readLine();</span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"서버 Echo 도착"</span>, fos);</span><br><span class="line">            Utils.clientTimeStamp(<span class="string">"서버 Echo msg: "</span> + messageFromServer, fos);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="Classic-IO-Single-Thread-ServerSocket"><a href="#Classic-IO-Single-Thread-ServerSocket" class="headerlink" title="Classic IO - Single Thread ServerSocket"></a>Classic IO - Single Thread ServerSocket</h1><p>이제 서버를 만들어 보자. 1번 타자는 Classic IO(또는 BIO(Blocking IO))로 만든 울트라 심플 싱글 스레드 소켓 서버다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> io.homo_efficio.server.socket;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> io.homo_efficio.server.common.Constants;</span><br><span class="line"><span class="keyword">import</span> io.homo_efficio.server.common.EchoProcessor;</span><br><span class="line"><span class="keyword">import</span> io.homo_efficio.server.common.Utils;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.FileOutputStream;</span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.net.ServerSocket;</span><br><span class="line"><span class="keyword">import</span> java.net.Socket;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> homo.efficio@gmail.com</span></span><br><span class="line"><span class="comment"> * created on 2020-10-10</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">EchoSocketServerSingleThread</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        EchoSocketServerSingleThread echoSocketServerSingleThread = <span class="keyword">new</span> EchoSocketServerSingleThread();</span><br><span class="line">        echoSocketServerSingleThread.start();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> (ServerSocket serverSocket = <span class="keyword">new</span> ServerSocket(Constants.SERVER_PORT);</span><br><span class="line">             FileOutputStream fos = Utils.getCommonFileOutputStream()</span><br><span class="line">        ) &#123;</span><br><span class="line">            Utils.serverTimeStamp(<span class="string">"==============================="</span>, fos);</span><br><span class="line">            Utils.serverTimeStamp(<span class="string">"Echo Server 시작"</span>, fos);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"---------------------------"</span>, fos);</span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"Single Thread Socket Echo Server 대기 중"</span>, fos);</span><br><span class="line">                <span class="comment">// accept() 는 연결 요청이 올 때까지 return 하지 않고 blocking</span></span><br><span class="line">                Socket acceptedSocket = serverSocket.accept();</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 연결 요청이 오면 accept() 가 반환하고 요청 처리 로직 수행</span></span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"Client 접속!!!"</span>, fos);</span><br><span class="line"><span class="comment">//            Utils.sleep(50L);</span></span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"Echo 시작"</span>, fos);</span><br><span class="line">                EchoProcessor.echo(acceptedSocket);</span><br><span class="line">                Utils.serverTimeStamp(<span class="string">"Echo 완료"</span>, fos);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ServerSocket</code>으로 서버 소켓을 생성하고, <code>accept()</code>로 클라이언트의 연결을 기다리고, 연결이 오면 클라이언트에게 메시지를 메아리로 되돌려 준다.</p><p>메아리를 담당하는 EchoProcessor는 다음과 같다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">EchoProcessor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> FileOutputStream fos = Utils.getCommonFileOutputStream();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">echo</span><span class="params">(Socket socket)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> (BufferedReader in = <span class="keyword">new</span> BufferedReader(<span class="keyword">new</span> InputStreamReader(socket.getInputStream()));</span><br><span class="line">             PrintWriter out = <span class="keyword">new</span> PrintWriter(socket.getOutputStream())</span><br><span class="line">        ) &#123;</span><br><span class="line">            String clientMessage = in.readLine();  <span class="comment">// in에 읽을 게 들어올 때까지 blocking</span></span><br><span class="line">            String serverMessage = <span class="string">"Server Echo - "</span> + clientMessage + System.lineSeparator();</span><br><span class="line">            out.println(serverMessage);</span><br><span class="line">            out.flush();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Socket</code>을 인자로 받고, 소켓에서 Reader, Writer를 뽑아내서, Reader에서 메아리를 읽고 ‘Server Echo -‘라는 문자열을 앞에 붙여서 Writer로 회신한다.</p><p>여기서 주의할 점이 있다. <strong>서버가 보내는 메시지에 비어 있는 행이 포함돼야 클라이언트가 <code>readLine()</code>으로 읽을 때 행을 구별해서 문제 없이 읽고 출력할 수 있다.</strong> 비어 있는 행이 없으면 클라이언트의 <code>readLine()</code>이 계속 비어 있는 행을 기다리면서 서버와의 연결을 점유하게 되고, 싱글 스레드인 서버는 먹통 상태가 된다.</p><h1 id="실습"><a href="#실습" class="headerlink" title="실습"></a>실습</h1><ol><li><p>EchoSocketServerSingleThread 를 실행하고, EchoSocketClient 를 실행하면 temp.log 파일에 다음과 같이 로그가 찍한다.</p> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">[SERVER -            main] 2020-11-01T23:49:25.119684 - ===============================</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:25.133603 - Echo Server 시작</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:25.133994 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:25.134174 - Single Thread Socket Echo Server 대기 중</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:26.976560 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:26.976861 - Echo 시작</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:26.992329 - Client 시작</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:27.006950 - 메시지 전송 시작</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:27.007250 - 메시지 print 완료</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:27.008839 - 메시지 flush 완료</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:27.009160 - 서버 Echo 대기...</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:27.020318 - 서버 Echo 도착</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:27.021049 - Echo 완료</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:27.021302 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:27.021471 - Single Thread Socket Echo Server 대기 중</span><br><span class="line">[CLIENT -            main] 2020-11-01T23:49:27.021674 - 서버 Echo msg: Server Echo - 안녕, echo server</span><br></pre></td></tr></table></figure></li><li><p>서버는 여전히 대기 중이므로 다른 터미널에서 <code>echo -n &#39;아무거나&#39; | nc localhost 7777</code>을 입력하면 다음과 같이 Echo 메시지가 바로 출력되어 나온다.</p> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">다른 터미널창</span><br><span class="line">🍺🦑🍺🍕🍺 ❯ echo -n &apos;아무거나&apos; | nc localhost 7777                                             </span><br><span class="line">Server Echo - 아무거나</span><br></pre></td></tr></table></figure> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">... 윗 부분 생략 ...</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:27.021302 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-01T23:49:27.021471 - Single Thread Socket Echo Server 대기 중</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">[SERVER -            main] 2020-11-02T00:03:47.863975 - Client 접속!!!</span><br><span class="line">[SERVER -            main] 2020-11-02T00:03:47.874942 - Echo 시작</span><br><span class="line">[SERVER -            main] 2020-11-02T00:03:47.878276 - Echo 완료</span><br><span class="line">[SERVER -            main] 2020-11-02T00:03:47.878572 - ---------------------------</span><br><span class="line">[SERVER -            main] 2020-11-02T00:03:47.878849 - Single Thread Socket Echo Server 대기 중</span><br></pre></td></tr></table></figure></li><li><p>EchoSocketClient 에서 <code>// Utils.sleep(5000L);  // 서버 blocking 확인 시 사용</code>라고 돼 있던 부분의 주석을 해제하고 실행해서 클라이언트가 서버와 연결된 후 5초 후에 서버에 메시지를 전송하도록 하고, 5초 안에 다른 터미널에서 <code>echo -n &#39;아무거나&#39; | nc localhost 7777</code>을 입력한다.  </p><ul><li>그러면 메아리가 터미널에 금방 출력되지 않고 5초 후에 출력된다.</li><li>이유는 앞서 말한 것처럼 EchoSocketClient가 5초 후에 메시지를 보내는 동안, EchoProcessor의 <code>in.readLine()</code>이 블로킹 상태로 대기하는데, 서버의 스레드도 1개 뿐이라 다른 요청을 <code>accept()</code> 할 수 없기 때문이다.</li><li>그래서 터미널 클라이언트도 5초간 블로킹 상태로 대기하게 된다.</li><li>결국 <strong>이상한 클라이언트가 하나 끼면 서버도 먹통되고 다른 클라이언트까지 먹통이 전파될 수 있다.</strong></li></ul></li></ol><h1 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h1><blockquote><ul><li>블로킹 방식의 싱글 스레드 소켓 서버는 시간 끄는 이상한 클라이언트가 하나만 들어와도 서버가 먹통이 되고, 다른 클라이언트까지 먹통될 수 있다.</li></ul></blockquote><p>이 문제는 어떻게 해결할까? <a href="https://homoefficio.github.io/2020/11/02/Back-to-the-Essence-Java-Servers-2/">2편</a>에서 알아보자.</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Back-to-the-Essence-Java-Servers-1편&quot;&gt;&lt;a href=&quot;#Back-to-the-Essence-Java-Servers-1편&quot; class=&quot;headerlink&quot; title=&quot;Back to the Essence - 
      
    
    </summary>
    
      <category term="Network" scheme="http://homoefficio.github.io/categories/Network/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="I/O" scheme="http://homoefficio.github.io/tags/I-O/"/>
    
      <category term="Java IO" scheme="http://homoefficio.github.io/tags/Java-IO/"/>
    
      <category term="Echo Server" scheme="http://homoefficio.github.io/tags/Echo-Server/"/>
    
      <category term="ServerSocket" scheme="http://homoefficio.github.io/tags/ServerSocket/"/>
    
      <category term="Socket" scheme="http://homoefficio.github.io/tags/Socket/"/>
    
      <category term="Blocking" scheme="http://homoefficio.github.io/tags/Blocking/"/>
    
      <category term="netcat" scheme="http://homoefficio.github.io/tags/netcat/"/>
    
      <category term="accept" scheme="http://homoefficio.github.io/tags/accept/"/>
    
  </entry>
  
  <entry>
    <title>Batch 작업과 Connection Pool</title>
    <link href="http://homoefficio.github.io/2020/08/27/Batch-%EC%9E%91%EC%97%85%EA%B3%BC-Connection-Pool/"/>
    <id>http://homoefficio.github.io/2020/08/27/Batch-작업과-Connection-Pool/</id>
    <published>2020-08-27T04:28:29.000Z</published>
    <updated>2022-03-18T16:07:46.164Z</updated>
    
    <content type="html"><![CDATA[<h2 id="커넥션풀"><a href="#커넥션풀" class="headerlink" title="커넥션풀"></a>커넥션풀</h2><p>일반적으로 DB에 연결해서 어떤 작업을 할 때는 커넥션풀(Connection Pool)을 사용한다. DB 연결 자체가 비용이 많이 들기 때문에 미리 다수의 Connection 객체를 만들어서 풀에 넣어두고 필요할 때마다 꺼내쓰고 반납하기를 반복한다. 결국 응답 속도를 빠르게 하고 자원 효율성을 높이기 위해 커넥션풀을 사용한다.</p><p>커넥션풀을 사용하면 미리 만들어진 연결을 여러 곳에서 재사용하기 때문에 연결 객체에 어떤 공통의 상태를 두고 이를 변경해가면서 사용하면 안 된다. A라는 작업에서 필요에 의해 그 상태를 변경하면, A가 쓰고 반납한 연결 객체를 재사용하는 B라는 다른 작업에서는 A에 의해 변경된 상태에 의해 의도하지 않게 동작할 위험이 있기 때문이다.</p><h2 id="배치-작업"><a href="#배치-작업" class="headerlink" title="배치 작업"></a>배치 작업</h2><p>하지만 배치(bach) 작업은 어떨까? 대부분의 배치 작업은 최종 사용자의 요청과는 무관하게 동작하며 따라서 최종 사용자에게 필요한 수준의 응답 속도를 필요로 하지 않는다. 그리고 보통 작업량도 많아서 DB 연결 생성 비용은 그 작업에 필요한 전체 비용과 비교하면 무시할 정도로 미미한 수준이다. 따라서 <strong>일반적인 배치 작업에서는 커넥션풀의 효용이 그다지 크지 않다.</strong></p><p>게다가 작업 성격 상 커넥션별로 어떤 설정값을 변경해야 하는 경우도 많다. 예를 들어 Hive의 <a href="https://github.com/HomoEfficio/dev-tips/blob/master/Hive%20Dynamic%20Partition%20Insert.md" target="_blank" rel="noopener">Dynamic Partition Insert</a>을 사용할 때는 아래 <code>set hive.exec.XXX=YYY</code>와 같이 설정값을 변경해줘야 한다.</p><p><img src="https://i.imgur.com/gTXD7Sp.png" alt="Imgur"></p><p><strong>a, b로 설정한 게 c 실행 시 까지 유효해야 하는데, 실제로는 a, b, c 모두 서로 다른 커넥션에서 실행된다.</strong> 이는 크게 세 가지 치명적인 문제를 유발한다.</p><ol><li>a, b 설정 내용은 해당 커넥션에 그대로 남아서 나중에 다른 작업에 영향을 미친다. 다른 작업은 의도한 것과 다르게 동작할 수 있다는 얘기다.</li><li>a, b 설정 내용은 c 실행 시에는 적용되지 않는다. 셋 모두 별개의 커넥션에서 실행되기 때문이다.</li><li>1, 2에 의한 문제는 간헐적, 우발적으로 발생한다. 재연이 어렵고 디버깅이 어렵다.</li></ol><p>참고로 a, b, c 실행 시 서로 다른 커넥션이 사용된다는 건 아래의 JdbcTemplate 구현 내용에서 확인할 수 있다.</p><p><img src="https://i.imgur.com/A3LyoRc.png" alt="Imgur"></p><p>위와 같이 결국 JdbcTemplate의 DB 작업 메서드가 실행될 때마다 <code>DataSourceUtil.getConnection()</code>로 커넥션을 가져오는데, 이게 결국 커넥션풀에서 그때그때 커넥션을 새로 가져온다.</p><h2 id="배치-작업에서의-커넥션풀"><a href="#배치-작업에서의-커넥션풀" class="headerlink" title="배치 작업에서의 커넥션풀"></a>배치 작업에서의 커넥션풀</h2><p>앞에서 얘기한 것처럼 배치 작업에서의 커넥션풀의 효용은 크지 않다. 그리고 커넥션별 설정 변경 같은 사용 사례가 필요한 상황에서는 커넥션풀을 써서 커넥션을 재사용하는 것은 앞에서 얘기한 것처럼 해결이 어려운 문제를 유발하는 부작용만 떠안을 뿐이다.</p><p>그럼 커넥션풀을 안 쓰면 되는 거 아닌가? 맞다. 근데 실무적으로 편하게 사용할 수 있는 JdbcTemplate이 커넥션풀에서 커넥션을 가져오게 되어 있으므로 이 편리함을 그대로 유지하려면 커넥션풀을 사용해야 한다. 뭐야 그럼 어쩌라고?</p><p>커넥션풀을 사용하되 적당히 설정하면 커넥션을 사용할 때마다 커넥션 객체를 새로 생성하게 할 수 있다.</p><p>즉 커넥션풀을 사용하지만 일반적인 커넥션풀처럼 커넥션을 미리 만들어두고 재사용하는 게 아니라, </p><ul><li>커넥션을 사용할 때마다 늘 새로 만들고,</li><li>사용 후에 바로 폐기되며,</li><li>동시 사용 커넥션의 최대 갯수만 제한을 두는</li></ul><p>특별한 풀을 만들 수 있다.</p><p>아래는 Tomcat의 PoolProperties를 사용할 때의 설정값인데 다른 ConnectionPool 구현체를 쓰더라도 비슷한 설정방법이 있을 것이다.</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">driverClassName:</span> <span class="string">a.b.c.d.GoodDriver</span></span><br><span class="line"><span class="attr">url:</span> <span class="string">JDBC_URL</span></span><br><span class="line"><span class="attr">username:</span> <span class="string">USERNAME</span></span><br><span class="line"><span class="attr">password:</span> <span class="string">PASSWORD</span></span><br><span class="line"><span class="attr">initialSize:</span> <span class="number">0</span>  <span class="comment"># 풀 생성 시 커넥션 객체를 미리 생성하지 않음</span></span><br><span class="line"><span class="attr">maxActive:</span> <span class="number">100</span>  <span class="comment"># 동시 사용 가능 한 커넥션 객체의 갯수 상한값을 기본값인 100으로 명시</span></span><br><span class="line"><span class="attr">maxIdle:</span> <span class="number">0</span>      <span class="comment"># 사용 후 대기 중인 커넥션의 최대 갯수를 0으로 고정</span></span><br><span class="line"><span class="attr">minIdle:</span> <span class="number">0</span>      <span class="comment"># 사용 후 대기 중인 커넥션의 최소 갯수를 0으로 고정</span></span><br><span class="line"><span class="attr">minEvictableIdleTimeMillis:</span> <span class="number">0</span>  <span class="comment"># 사용 후 폐기 전 대기 시간을 0밀리초로 고정 -&gt; 사용 후 바로 폐기</span></span><br></pre></td></tr></table></figure><h2 id="Tx-처리"><a href="#Tx-처리" class="headerlink" title="Tx 처리"></a>Tx 처리</h2><p>위와 같이 커넥션풀을 설정하면 앞에서 살펴본 1번 문제는 해결할 수 있다. 하지만 a, b 설정이 c 실행시까지 유지되어야 하는 2번 문제는 어떻게 해결해야 할까?</p><p>하나의 Tx로 묶어주면 JdbcTemplate이 처음에 가져온 커넥션을 Tx 종료시까지 계속 사용할 수 있다. 스프링이라면 <code>@Transactional</code>을 사용하거나 아래와 같이 PlatformTransactionManager 를 사용해서 하나의 Tx로 묶어줄 수 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">TransactionStatus transactionStatus = <span class="keyword">this</span>.transactionManager.getTransaction(<span class="keyword">new</span> DefaultTransactionDefinition());</span><br><span class="line"></span><br><span class="line">JdbcTemplate hiveTezJdbcTemplate = hiveTezDataReader.createJdbcTemplate();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 아래 3개의 명령이 모두 동일한 커넥션 객체를 통해 실행됨</span></span><br><span class="line">hiveTezJdbcTemplate.execute(<span class="string">"set hive.exec.dynamic.partition.mode=nonstrict"</span>);</span><br><span class="line">hiveTezJdbcTemplate.execute(<span class="string">"set hive.exec.max.dynamic.partitions=50000"</span>);</span><br><span class="line">hiveTezJdbcTemplate.execute(hiveCustomerConsumptionQuery);</span><br><span class="line"></span><br><span class="line"><span class="keyword">this</span>.transactionManager.commit(transactionStatus);</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;커넥션풀&quot;&gt;&lt;a href=&quot;#커넥션풀&quot; class=&quot;headerlink&quot; title=&quot;커넥션풀&quot;&gt;&lt;/a&gt;커넥션풀&lt;/h2&gt;&lt;p&gt;일반적으로 DB에 연결해서 어떤 작업을 할 때는 커넥션풀(Connection Pool)을 사용한다. DB 연결 
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="JDBC" scheme="http://homoefficio.github.io/tags/JDBC/"/>
    
      <category term="Batch" scheme="http://homoefficio.github.io/tags/Batch/"/>
    
      <category term="ConnectionPool" scheme="http://homoefficio.github.io/tags/ConnectionPool/"/>
    
      <category term="JdbcTemplate" scheme="http://homoefficio.github.io/tags/JdbcTemplate/"/>
    
      <category term="Hive" scheme="http://homoefficio.github.io/tags/Hive/"/>
    
  </entry>
  
  <entry>
    <title>Java NIO FileChannel 과 DirectByteBuffer</title>
    <link href="http://homoefficio.github.io/2020/08/10/Java-NIO-FileChannel-%EA%B3%BC-DirectByteBuffer/"/>
    <id>http://homoefficio.github.io/2020/08/10/Java-NIO-FileChannel-과-DirectByteBuffer/</id>
    <published>2020-08-10T14:44:59.000Z</published>
    <updated>2022-03-18T16:07:46.380Z</updated>
    
    <content type="html"><![CDATA[<p>Java 4에서 도입된 NIO 덕분에 <code>FileChannel</code>과 <code>ByteBuffer</code>를 이용해서 File I/O 를 수행할 수 있게 됐다.</p><p><img src="https://i.imgur.com/vEL6ni9.png" alt="Imgur"><br>그림 출처: <a href="https://www.happycoders.eu/java/filechannel-bytebuffer-memory-mapped-file-locks/" target="_blank" rel="noopener">https://www.happycoders.eu/java/filechannel-bytebuffer-memory-mapped-file-locks/</a></p><p>NIO의 장점은 <a href="https://homoefficio.github.io/2016/08/06/Java-NIO는-생각만큼-non-blocking-하지-않다/">https://homoefficio.github.io/2016/08/06/Java-NIO는-생각만큼-non-blocking-하지-않다/</a> 를 참고하고, 여기에서는 <code>FileChannel</code>과 <code>DirectBuffer</code> 얘기만 다룬다.</p><p><code>ByteBuffer</code>는 생성되는 위치를 기준으로 크게 나눠보면 JVM Heap 내에 생성되는 <code>HeapByteBuffer</code>와 JVM Heap 밖에 있는 Native 공간에 생성되는 <code>DirectByteBuffer</code>로 나눌 수 있다. 아래 그림에는 먼저 <code>HeapByteBuffer</code>와 <code>MappedByteBuffer</code>로 구분되는 걸로 보이는데 <code>MappedByteBuffer</code>도 Native 공간에 생성되며 파일 일부를 메모리에 매핑한다는 점 외에는 일반적인 direct byte buffer 와 동작이 다르지 않다고 <a href="https://docs.oracle.com/javase/8/docs/api/java/nio/MappedByteBuffer.html" target="_blank" rel="noopener">API 문서</a>에 나와있다.</p><p><img src="https://i.imgur.com/AE4p00B.png" alt="Imgur"></p><p>위 그림에는 안 나와있지만 <code>DirectByteBuffer</code>는 <code>DirectBuffer</code> 인터페이스를 구현하고 있다.</p><p><code>HeapByteBuffer</code>를 사용하면 JVM의 GC에 안전하게 의지할 수 있지만, CPU 개입 없이 I/O를 수행할 수 있고 불필요한 copy 부하가 발생하지 않아 성능적으로 유리한 점이 많은 DMA(DirectMemoryAccess)는 활용할 수 없다.</p><p>반대로 <code>DirectByteBuffer</code>를 사용하면 DMA의 혜택을 얻을 수 있지만, JVM의 GC를 벗어나게 되므로 메모리 관리 부담이 생겨난다.</p><p>그래서 <code>FileChannel</code> 을 사용할 때 상황에 맞게 <code>HeapByteBuffer</code>나 <code>DirectByteBuffer</code> 중에 골라서 쓰면 될 것 같..지만, 실상은 꼭 그렇지는 않다!</p><p>이제부터 코드와 함께 그 내부를 살짝 들여다보자. 앞으로 나오는 내용은 모두 Java 8 기준이다.</p><h1 id="FileChannel-write-ByteBuffer"><a href="#FileChannel-write-ByteBuffer" class="headerlink" title="FileChannel.write(ByteBuffer)"></a>FileChannel.write(ByteBuffer)</h1><p><code>HeapByteBuffer</code>에 담겨 있는 내용을 파일에 저장하려면 대략 아래와 같은 코드를 사용하게 된다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">String contents = <span class="string">"abcde"</span>;</span><br><span class="line"><span class="keyword">byte</span>[] byteArr = contents.getBytes(StandardCharsets.UTF_8);</span><br><span class="line"></span><br><span class="line">ByteBuffer byteBuffer = ByteBuffer.wrap(byteArr);  <span class="comment">// HeapByteBuffer를 반환한다.</span></span><br><span class="line"><span class="comment">// 또는</span></span><br><span class="line"><span class="comment">// ByteBuffer byteBuffer = ByteBuffer.allocate(byteArr.length);    // HeapByteBuffer를 반환한다.</span></span><br><span class="line"><span class="comment">// byteBuffer.put(byteArr);</span></span><br><span class="line"><span class="comment">// byteBuffer.flip();</span></span><br><span class="line"></span><br><span class="line">System.out.println(<span class="string">"isDirect? "</span> + byteBuffer.isDirect());  <span class="comment">// false</span></span><br><span class="line">fileChannel.write(byteBuffer);</span><br></pre></td></tr></table></figure><p>위 코드에는 <code>DirectByteBuffer</code>가 전혀 나오지 않는다.</p><p>그런데 막상 실행해서 <a href="https://homoefficio.github.io/2020/04/09/Java-Native-Memory-Tracking/">jcmd로 Native 메모리를 모니터링</a> 해보면, <code>DirectByteBuffer</code>가 사용하는 Native 메모리를 나타내는 Internal 항목이 위 사용한 Buffer의 크기만큼 증가하는 것을 확인할 수 있다. 대략 다음과 같은 내용이 표시된다.</p><p><img src="https://i.imgur.com/t9gmDhx.png" alt=""></p><p>분명히 <code>HeapByteBuffer</code>를 사용했는데 왜 Native 메모리가 동원되는 걸까?</p><h1 id="FileChannelImpl-write-ByteBuffer-와-그-이후"><a href="#FileChannelImpl-write-ByteBuffer-와-그-이후" class="headerlink" title="FileChannelImpl.write(ByteBuffer) 와 그 이후"></a>FileChannelImpl.write(ByteBuffer) 와 그 이후</h1><p>특별히 지정하지 않는다면 인터페이스인 <code>FileChannel</code>의 구현체로 <code>sun.nio.ch.FileChannelImpl</code>이 사용된다. 코드는 아래와 같다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.FileChannelImpl</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">write</span><span class="params">(ByteBuffer src)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        ensureOpen();</span><br><span class="line">        <span class="keyword">if</span> (!writable)</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> NonWritableChannelException();</span><br><span class="line">        <span class="keyword">synchronized</span> (positionLock) &#123;</span><br><span class="line">            <span class="keyword">int</span> n = <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">int</span> ti = -<span class="number">1</span>;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                begin();</span><br><span class="line">                ti = threads.add();</span><br><span class="line">                <span class="keyword">if</span> (!isOpen())</span><br><span class="line">                    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">                <span class="keyword">do</span> &#123;</span><br><span class="line">                    n = IOUtil.write(fd, src, -<span class="number">1</span>, nd);  <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">                &#125; <span class="keyword">while</span> ((n == IOStatus.INTERRUPTED) &amp;&amp; isOpen());</span><br><span class="line">                <span class="keyword">return</span> IOStatus.normalize(n);</span><br><span class="line">            &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                threads.remove(ti);</span><br><span class="line">                end(n &gt; <span class="number">0</span>);</span><br><span class="line">                <span class="keyword">assert</span> IOStatus.check(n);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><code>IOUtil.write()</code>는 다음과 같다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.IOUtil</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">write</span><span class="params">(FileDescriptor fd, ByteBuffer src, <span class="keyword">long</span> position,</span></span></span><br><span class="line"><span class="function"><span class="params">                     NativeDispatcher nd)</span></span></span><br><span class="line"><span class="function">        <span class="keyword">throws</span> IOException</span></span><br><span class="line"><span class="function">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (src <span class="keyword">instanceof</span> DirectBuffer)</span><br><span class="line">            <span class="keyword">return</span> writeFromNativeBuffer(fd, src, position, nd);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Substitute a native buffer</span></span><br><span class="line">        <span class="keyword">int</span> pos = src.position();</span><br><span class="line">        <span class="keyword">int</span> lim = src.limit();</span><br><span class="line">        <span class="keyword">assert</span> (pos &lt;= lim);</span><br><span class="line">        <span class="keyword">int</span> rem = (pos &lt;= lim ? lim - pos : <span class="number">0</span>);</span><br><span class="line">        ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);  <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            bb.put(src);</span><br><span class="line">            bb.flip();</span><br><span class="line">            <span class="comment">// Do not update src until we see how many bytes were written</span></span><br><span class="line">            src.position(pos);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">int</span> n = writeFromNativeBuffer(fd, bb, position, nd);</span><br><span class="line">            <span class="keyword">if</span> (n &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                <span class="comment">// now update src</span></span><br><span class="line">                src.position(pos + n);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> n;</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            Util.offerFirstTemporaryDirectBuffer(bb);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>오호 특이한 점이 눈에 들어온다. 인자로 받아온 <code>ByteBuffer</code>의 Type이 <code>DirectBuffer</code>이면 <code>writeFromNativeBuffer()</code>를 호출하고 반환하지만, <code>DirectBuffer</code>가 아니면 <code>Util.getTemporaryDirectBuffer(rem)</code> 이렇게 슬그머니 <code>DirectBuffer</code>를 생성한다!!</p><p><img src="https://i.imgur.com/72E8YHH.jpg?1" alt="Imgur"></p><p>잠시 곁다리로 빠져보자면, 몰래 대체품을 만들어 쓰기는 하지만 그래도 양심은 있는지 다음과 같이 개발자가 <code>HeapByteBuffer</code> 생성 시 지정한 크기가 아니라 실제 read/write 할 데이터 크기만큼의 <code>DirectByteBuffer</code>만 생성하는 점은 그래도 높이 쳐줄 수 있다. 그런데 이마저도 나중에 살펴볼 <code>BufferCache</code> 동작 방식을 생각해보면 좋다고만 할 수는 없다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">int</span> rem = (pos &lt;= lim ? lim - pos : <span class="number">0</span>);  <span class="comment">// 버퍼의 크기가 아니라 실제 read/write 해야할 데이터 크기(limit - position)</span></span><br><span class="line">ByteBuffer bb = Util.getTemporaryDirectBuffer(rem);  <span class="comment">//&lt;=== 여기!!!</span></span><br></pre></td></tr></table></figure><p>이어서 계속 따라가보자. <code>Util.getTemporaryDirectBuffer(rem)</code>은 다음과 같다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.Util</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ByteBuffer <span class="title">getTemporaryDirectBuffer</span><span class="params">(<span class="keyword">int</span> size)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// If a buffer of this size is too large for the cache, there</span></span><br><span class="line">        <span class="comment">// should not be a buffer in the cache that is at least as</span></span><br><span class="line">        <span class="comment">// large. So we'll just create a new one. Also, we don't have</span></span><br><span class="line">        <span class="comment">// to remove the buffer from the cache (as this method does</span></span><br><span class="line">        <span class="comment">// below) given that we won't put the new buffer in the cache.</span></span><br><span class="line">        <span class="keyword">if</span> (isBufferTooLarge(size)) &#123;</span><br><span class="line">            <span class="keyword">return</span> ByteBuffer.allocateDirect(size);  <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        BufferCache cache = bufferCache.get();</span><br><span class="line">        ByteBuffer buf = cache.get(size);</span><br><span class="line">        <span class="keyword">if</span> (buf != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> buf;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// No suitable buffer in the cache so we need to allocate a new</span></span><br><span class="line">            <span class="comment">// one. To avoid the cache growing then we remove the first</span></span><br><span class="line">            <span class="comment">// buffer from the cache and free it.</span></span><br><span class="line">            <span class="keyword">if</span> (!cache.isEmpty()) &#123;</span><br><span class="line">                buf = cache.removeFirst();</span><br><span class="line">                free(buf);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> ByteBuffer.allocateDirect(size);  <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>대충 뭔가 재사용을 위해 캐시 개념을 사용하는 것 같은데 여튼 결국에는 <code>ByteBuffer.allocateDirect(size)</code>를 호출해서 <code>DirectByteBuffer</code>를 생성한다. <code>ByteBuffer.allocateDirect(size)</code>는 다음과 같다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// java.nio.ByteBuffer</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ByteBuffer <span class="title">allocateDirect</span><span class="params">(<span class="keyword">int</span> capacity)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> DirectByteBuffer(capacity);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>즉, 특별한 설정 없이 <strong>일반적인 상황에서 <code>FileChannel.write()</code>을 사용하면 개발자가 작성한 프로그램 코드에서 <code>HeapByteBuffer</code>를 사용했더라도 내부적으로는 그 <code>HeapByteBuffer</code>가 사용되지 않고 항상 <code>DirectByteBuffer</code>가 사용된다.</strong> 코드 확인 결과 <strong><code>FileChannel.read()</code>도 마찬가지</strong>다.</p><p>상당히 당황스럽다. 게다가 이런 얘기를 왜 API 문서나 기타 자료에서 쉽게 접할 수 없는지는 솔직히 의문이다.</p><p>암튼 그래.. 내가 쓰라고 한 건 무시하고 나 몰래 <code>DirectByteBuffer</code>를 만들어서 사용하네.. 근데 뭐가 문제임? 어쨌거나 잘 돌면 되는 거 아님?</p><p>이제부터 현장에서 발생했던 사례 얘기를 풀어본다. 만약 늘 아래 사례와 같이 동작한다면 상당히 심각한 버그라고 볼 수 있는데, 워낙 개발을 못 하는지라 어느 부분에선가 내가 코드를 잘못 짰을 수도 있기 때문에 늘 발생하는 상황이라고 단정할 수는 없다. 어쨌든 호기심이 생긴다면 이어서 쭉 보자.</p><h1 id="DirectByteBuffer-메모리-회수"><a href="#DirectByteBuffer-메모리-회수" class="headerlink" title="DirectByteBuffer 메모리 회수"></a>DirectByteBuffer 메모리 회수</h1><p>앞에서 <code>DirectByteBuffer</code>를 사용하면 DMA 혜택을 얻지만 메모리 관리 부담이 생긴다고 했다. <code>DirectByteBuffer</code> 메모리는 어떻게 회수되는 걸까? 여러 자료 찾아봤는데 대략 이런 말로 귀결된다.</p><blockquote><p>Native 메모리를 참조하는 객체는 결국 JVM Heap 안에 생성되며,<br>이 객체가 JVM의 GC에 의해 회수되면 이 객체가 참조하는 Native 메모리는<br>JVM이 아닌 다른 메커니즘에 의해 어쨌든 회수된다.</p></blockquote><p>요는 <strong><code>DirectByteBuffer</code>를 사용해도 간접적이긴 하지만 결국에는 JVM GC에 의해 회수가 시작된다</strong>는 얘기다. 오 그럼 바로 회수되는 건 아니지만 다행스럽게도 결국 JVM GC가 챙겨주시는 거나 마찬가지네~</p><p>그런데 위 설명과는 다르게 어느 정도 시간이 지나면 결국 늘 이 분을 영접하게 되었다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java.lang.OutOfMemoryError: Direct buffer memory</span><br></pre></td></tr></table></figure><p>처음에는 ‘아 왜요~~ 저 <code>DirectByteBuffer</code> 쓰지도 않는데 저한테 왜 이러세요 진짜~‘ 였다. 그런데 에러 로그를 따라가보니 위에서 설명한 것처럼 나 몰래 응큼하게 내부적으로 <code>DirectByteBuffer</code>가 사용된다는 것까지는 알게 되었다. 그런데 메모리 반납은? 나 몰래 만들었으면 나 몰래 반납도 해줘야 하는 거 아님? 난 쪼렙 하수지만 넌 신성한 JDK 잖아!</p><p>여러 번 테스트 해봤는데 몇 시간, 심지어 며칠이 지나도 반납 안 해주더라.. JDK고 나발이고 나는 분명히 <code>HeapByteBuffer</code>를 전달해줬는데 나 몰래 <code>DirectByteBuffer</code>랑 바람 피우고, 그것도 모자라 지가 쓴 카드값까지 나한테..</p><p><img src="https://i.imgur.com/Nejwtsv.png" alt="Imgur"></p><p>어쨌든 상황을 정리해보면 다음과 같다.</p><blockquote><p>일반적으로 <code>FileChannel</code>에 데이터를 write할 때는 결국 항상 <code>DirectByteBuffer</code>가 사용되는데,<br><code>OutOfMemoryError: Direct buffer memory</code>가 계속 발생하는 걸로 봐서는,<br><code>DirectByteBuffer</code>로 사용된 Native 메모리가 제대로 회수되지 않는(것 같)다.</p></blockquote><p>자바에 내가 명시적으로 GC를 확실하게 유발할 수 있는 수단이 있는 것도 아닌데.. 망했.. 이러면 <code>FileChannel</code>은 못 쓰는 건데.. API 문서에도 다른 자료에도 왜 시원한 해법이 없지? 설마 아무도 <code>FileChannel</code>을 안 쓰는 건가? 그럴리가.. 내가 어딘가 잘못 짠 거겠지..</p><p>별 생각이 다 드는 가운데 여기서 대반전!</p><h1 id="DirectByteBuffer-메모리-회수-방법"><a href="#DirectByteBuffer-메모리-회수-방법" class="headerlink" title="DirectByteBuffer 메모리 회수 방법"></a>DirectByteBuffer 메모리 회수 방법</h1><p><code>DirectByteBuffer</code> 메모리는, JVM의 Heap 밖에 있어서 JVM GC가 아닌 다른 메커니즘에 의해 회수된다는 그 <strong><code>DirectByteBuffer</code> 메모리는, 놀랍게도 Java 코드로 명시적으로 바로 회수할 수 있는 방법이 있었다.</strong> 아무리 검색해도 찾을 수가 없던 희귀한 내용이지만 바로 공유한다. 위에서 살펴본 코드 중에 답이 있었다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.Util</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ByteBuffer <span class="title">getTemporaryDirectBuffer</span><span class="params">(<span class="keyword">int</span> size)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// If a buffer of this size is too large for the cache, there</span></span><br><span class="line">        <span class="comment">// should not be a buffer in the cache that is at least as</span></span><br><span class="line">        <span class="comment">// large. So we'll just create a new one. Also, we don't have</span></span><br><span class="line">        <span class="comment">// to remove the buffer from the cache (as this method does</span></span><br><span class="line">        <span class="comment">// below) given that we won't put the new buffer in the cache.</span></span><br><span class="line">        <span class="keyword">if</span> (isBufferTooLarge(size)) &#123;</span><br><span class="line">            <span class="keyword">return</span> ByteBuffer.allocateDirect(size);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        BufferCache cache = bufferCache.get();</span><br><span class="line">        ByteBuffer buf = cache.get(size);</span><br><span class="line">        <span class="keyword">if</span> (buf != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> buf;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// No suitable buffer in the cache so we need to allocate a new</span></span><br><span class="line">            <span class="comment">// one. To avoid the cache growing then we remove the first</span></span><br><span class="line">            <span class="comment">// buffer from the cache and free it.</span></span><br><span class="line">            <span class="keyword">if</span> (!cache.isEmpty()) &#123;</span><br><span class="line">                buf = cache.removeFirst();</span><br><span class="line">                free(buf);            <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> ByteBuffer.allocateDirect(size);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>엉? <code>free(buf)</code>가 있네? 어떻게 생겼나 한 번 볼까? 부왘ㅋㅋ 대박!!</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.Util</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">free</span><span class="params">(ByteBuffer buf)</span> </span>&#123;</span><br><span class="line">        ((DirectBuffer)buf).cleaner().clean();</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>응큼하게 바람 피우고 카드값 떠넘기는 <code>HeapByteBuffer</code>와 결별(사실 <code>HeapByteBuffer</code>는 죄가 없다. <code>FileChannel</code> 구현부가 죄인이지)하고 내가 그냥 <code>DirectByteBuffer</code>와 사랑에 빠지기로 했다. 그래서 다음과 같이 <code>HeapByteBuffer</code>를 사용하던 코드를</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">ByteBuffer buf1 = ByteBuffer.allocate(size);</span><br><span class="line">...</span><br><span class="line">buf1.flip();</span><br><span class="line">fileChannel.write(buf1);</span><br><span class="line">...</span><br><span class="line">ByteBuffer buf2 = ByteBuffer.wrap(byteArray);</span><br><span class="line">fileChannel.write(buf2);</span><br></pre></td></tr></table></figure><p>다음과 같이 명시적으로 <code>DirectByteBuffer</code>를 생성하고 사용하고 회수하도록 모두 바꾸고나니,</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">  ByteBuffer directBuffer = ByteBuffer.allocateDirect(size);</span><br><span class="line">  ...</span><br><span class="line">  directBuffer.flip();</span><br><span class="line">  fileChannel.write(directBuffer);</span><br><span class="line">  ...</span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">  ...</span><br><span class="line">&#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">  ((DirectBuffer)directBuffer).cleaner().clean();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>놀랍게도 <strong><code>((DirectBuffer)directBuffer).cleaner().clean()</code>가 호출된 후 <code>jcmd</code>로 확인해보면 Internal 영역이 명시적으로 사용한 <code>DirectByteBuffer</code> 크기만큼 바로 줄어드는 것을 확인할 수 있었다.</strong> 그리고 영 반갑지 않은 <code>java.lang.OutOfMemoryError: Direct buffer memory</code>도 다시 볼 일 없게 됐다.</p><p>이렇게 간단하고 직접적인 해결 방법이 이미 존재하는데 왜 그런 게 API 문서에 언급조차 없는지, 그리고 그 해결법도 왜 <code>public static</code>이 아니라 <code>private static</code> 메서드로 선언해둔 건지는 여전히 의문이다. 이 정도면 거의 일부러 감춰둔 정도 같기도 해서 ‘이거 써도 되는 거야?’라는 의문조차 들 정도..</p><p>그런데 한 가지 궁금한 게 더 있다.</p><p><code>HeapByteBuffer</code>를 전달해줘도 내부적으로 응큼하게 <code>DirectByteBuffer</code>를 몰래 만드는 <code>getTemporaryDirectBuffer()</code> 내부에서, <code>DirectByteBuffer</code> 메모리를 회수할 수 있는 <code>free()</code> 메서드가 호출되고 있음에도 불구하고 몰래 만들어진 <code>DirectByteBuffer</code>가 회수되지 않는 이유는 뭘까? 이건 <code>BufferCache</code>를 보면 알 수 있다.</p><h1 id="BufferCache"><a href="#BufferCache" class="headerlink" title="BufferCache"></a>BufferCache</h1><p>코드를 다시 보자.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.Util</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ByteBuffer <span class="title">getTemporaryDirectBuffer</span><span class="params">(<span class="keyword">int</span> size)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// If a buffer of this size is too large for the cache, there</span></span><br><span class="line">        <span class="comment">// should not be a buffer in the cache that is at least as</span></span><br><span class="line">        <span class="comment">// large. So we'll just create a new one. Also, we don't have</span></span><br><span class="line">        <span class="comment">// to remove the buffer from the cache (as this method does</span></span><br><span class="line">        <span class="comment">// below) given that we won't put the new buffer in the cache.</span></span><br><span class="line">        <span class="keyword">if</span> (isBufferTooLarge(size)) &#123;</span><br><span class="line">            <span class="keyword">return</span> ByteBuffer.allocateDirect(size);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        BufferCache cache = bufferCache.get();</span><br><span class="line">        ByteBuffer buf = cache.get(size);</span><br><span class="line">        <span class="keyword">if</span> (buf != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> buf;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment">// No suitable buffer in the cache so we need to allocate a new</span></span><br><span class="line">            <span class="comment">// one. To avoid the cache growing then we remove the first</span></span><br><span class="line">            <span class="comment">// buffer from the cache and free it.</span></span><br><span class="line">            <span class="keyword">if</span> (!cache.isEmpty()) &#123;</span><br><span class="line">                buf = cache.removeFirst();  <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">                free(buf);            <span class="comment">//&lt;=== 여기!!!</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> ByteBuffer.allocateDirect(size);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><code>BufferCache</code>에 동일한 크기의 <code>DirectByteBuffer</code>가 있으면 그걸 재사용하고, 없으면 캐시에 있는 다른 크기의 <code>DirectByteBuffer</code>를 <code>free()</code>를 이용해서 하나 삭제한다. 그렇게 해서 캐쉬의 총 갯수가 늘어나지 않게 한다. 실제로도 이렇게 잘 동작한다.</p><p>예를 들어 크기가 10M로 모두 같은 <code>HeapByteBuffer</code>를 3개 생성해서 <code>FileChannel.write()</code>에 사용하면 내부적으로 <code>DirectByteBuffer</code>가 생성되므로 10M 짜리 <code>DirectByteBuffer</code> 3개, 총 30M가 사용될 것 같지만, 위에 나오는 BufferCache 덕분에 실제로는 10M 짜리 <code>DirectByteBuffer</code> 한 개만 생성되고 재사용된다.</p><p>여기까지는 좋은데 문제는 맨 마지막 부분 <code>return ByteBuffer.allocateDirect(size)</code>에서 새로 생성된 후 반환되는 <code>DirectByteBuffer</code>는 회수되지 않는(걸로 보인)다는 점이다.</p><p><code>BufferCache</code>는 아래와 같이 <code>ThreadLocal</code>에 담겨서 per-thread로 존재한다. </p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// sun.nio.ch.Util</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Per-thread cache of temporary direct buffers</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> ThreadLocal&lt;BufferCache&gt; bufferCache =</span><br><span class="line">        <span class="keyword">new</span> ThreadLocal&lt;BufferCache&gt;()</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">protected</span> BufferCache <span class="title">initialValue</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> BufferCache();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br></pre></td></tr></table></figure><p>위에서 개발자가 명시적으로 생성한 <code>HeapByteBuffer</code> 대신 내부적으로 <code>DirectByteBuffer</code>를 생성할 때 실제 read/write 할 만큼의 <code>DirectByteBuffer</code>를 생성한다고 했다. 필요한 만큼만 새로 생성하므로 메모리 사용량에 있어서는 유리하지만, 그 필요한 만큼이 그때그때 다른 상황에서는 지금 살펴본 <code>BufferCache</code>의 hit율이 떨어져서 <code>DirectByteBuffer</code>의 생성 빈도가 많아질 수 있다. <a href="https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html" target="_blank" rel="noopener">ByteBuffer API</a> 문서에 따르면 <code>DirectByteBuffer</code>는 메모리 할당/해제 비용이 <code>HeapByteBuffer</code>보다 더 크다고 한다. 따라서 <code>DirectByteBuffer</code> 생성 빈도가 많으면 성능에 악영향을 미칠 수 있다. </p><p>정리하면, 하나의 스레드에서 이런 비명시적 방식(개발자가 <code>HeapByteBuffer</code>를 <code>FileChannel.write()</code>에 인자로 전달해줘도 <code>FileChannelImpl</code>이 내부적으로 몰래 <code>DirectByteBuffer</code>를 생성하는 방식)으로 <code>DirectByteBuffer</code>가 생성되면,  </p><ul><li>크기가 동일한 <code>HeapByteBuffer</code>를 여러개 만들어도  </li><li><code>BufferCache</code> 덕분에 해당 스레드 내에서는 <code>DirectByteBuffer</code>가 하나만 만들어지고 재사용될 수는 있지만,  </li><li>그 한 개의 <code>DirectByteBuffer</code>가 제대로 회수되지 않으면,</li><li>새로운 스레드가 실행될 때마다 계속 누적되다가 결국 OutOfMemory 에러를 맞이하게 된다.</li></ul><h1 id="XX-MaxDirectMemorySize-옵션"><a href="#XX-MaxDirectMemorySize-옵션" class="headerlink" title="XX:MaxDirectMemorySize 옵션"></a>XX:MaxDirectMemorySize 옵션</h1><p><code>DirectByteBuffer</code>가 사용하는 Native Memory 최대 크기를 지정할 수 있는 옵션이 있다. <code>XX:MaxDirectMemorySize</code>인데 <code>DirectByteBuffer</code> 메모리가 제대로 회수되지 않고 누적되다가 최대 크기를 넘는다면 어떻게 될까?</p><ol><li>오랫동안 사용되지 않고 메모리만 점유하고 있던 <code>DirectByteBuffer</code>를 알아서 회수한다.</li><li><code>java.lang.OutOfMemoryError: Direct buffer memory</code>가 발생한다.</li></ol><p>혹시 하는 마음에 1을 기대했는데, 현실은 2다.</p><h1 id="실제-검증-몰래-만들어진-DirectByteBuffer-메모리도-회수된다"><a href="#실제-검증-몰래-만들어진-DirectByteBuffer-메모리도-회수된다" class="headerlink" title="실제 검증 - 몰래 만들어진 DirectByteBuffer 메모리도 회수된다!!"></a>실제 검증 - 몰래 만들어진 <code>DirectByteBuffer</code> 메모리도 회수된다!!</h1><p>내가 잘못 짰을 수도 있는 로직과 뒤섞인 상태로는, 몰래 만들어지는 <code>DirectByteBuffer</code>가 항상 회수되지 않는다고 확언을 할 수 없으므로 실험용 간단한 프로그램을 만들어서 검증해봤다.</p><p><a href="https://github.com/HomoEfficio/scratchpad-bytebuffer" target="_blank" rel="noopener">https://github.com/HomoEfficio/scratchpad-bytebuffer</a> 를 참고하면 직접 요리조리 해볼 수 있다.</p><p>README에 있는대로, 프로그램 실행해서 VisualVM 으로 강제 GC를 시킨 후에 <code>jcmd</code>로 확인해보면 Native Memory가 회수되는 것을 확인할 수 있었다.</p><p>따라서 <strong>몰래 만들어진 <code>DirectByteBuffer</code>도 정상적인 경우라면 해당 <code>DirectByteBuffer</code>를 참조하는 객체(A라고 하자)가 GC될 때 <code>DirectByteBuffer</code> 메모리도 함께 회수된다.</strong> 라고 결론 지을 수 있겠다.</p><p>하지만 그래도 A가 언제 GC 될 지 알 수 없고, GC 전까지 <code>DirectByteBuffer</code>는 계속 Native Memory를 점유하게 된다. 아마도 잘못 작성한 코드 때문이겠지만 알 수 없는 이유로 Native Memory 가 장시간 계속 회수되지 않는다면, 우리에겐 <code>((DirectBuffer)directBuffer).cleaner().clean()</code> 라는 무기가 있다는 사실을 알아두면 좋다.</p><h1 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h1><blockquote><ul><li><p>특별한 <code>FileChannel</code> 구현체를 사용하지 않는다면, <code>FileChannel</code>에 read/write 할 때 <code>HeapByteBuffer</code>를 사용해도 내부적으로 <code>DirectByteBuffer</code>가 사용된다.</p></li><li><p>내부적으로 사용되는 <code>DirectByteBuffer</code>가 제대로 회수되지 않으면 <code>java.lang.OutOfMemoryError: Direct buffer memory</code>가 발생할 수 있다.</p></li><li><p>이렇게 <code>FileChannel</code>에 read/write 하는 코드에 의해 OOM이 발생한다면,</p><ul><li><code>HeapByteBuffer</code>을 사용하지 말고 명시적으로 <code>DirectByteBuffer</code>를 사용하고,  </li><li><code>((DirectBuffer)directBuffer).cleaner().clean()</code>를 사용해서 명시적으로 해제하자.</li></ul></li></ul></blockquote><h1 id="함께-보기"><a href="#함께-보기" class="headerlink" title="함께 보기"></a>함께 보기</h1><ul><li><a href="https://homoefficio.github.io/2016/08/06/Java-NIO는-생각만큼-non-blocking-하지-않다/">Java NIO는 생각만큼 non-blocking 하지 않다</a></li><li><a href="https://homoefficio.github.io/2019/02/27/Java-NIO-Direct-Buffer를-이용해서-대용량-파일-행-기준으로-쪼개기/">Java NIO Direct Buffer를 이용해서 대용량 파일 행 기준으로 쪼개기</a></li><li><a href="https://homoefficio.github.io/2016/08/13/대용량-파일을-AsynchronousFileChannel로-다뤄보기/">대용량 파일을 AsynchronousFileChannel로 다뤄보기</a></li><li><a href="https://homoefficio.github.io/2020/04/09/Java-Native-Memory-Tracking/">Java Native Memory Tracking</a></li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;Java 4에서 도입된 NIO 덕분에 &lt;code&gt;FileChannel&lt;/code&gt;과 &lt;code&gt;ByteBuffer&lt;/code&gt;를 이용해서 File I/O 를 수행할 수 있게 됐다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.imgur.com
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="Native Memory" scheme="http://homoefficio.github.io/tags/Native-Memory/"/>
    
      <category term="DirectBuffer" scheme="http://homoefficio.github.io/tags/DirectBuffer/"/>
    
      <category term="jcmd" scheme="http://homoefficio.github.io/tags/jcmd/"/>
    
      <category term="NIO" scheme="http://homoefficio.github.io/tags/NIO/"/>
    
      <category term="ByteBuffer" scheme="http://homoefficio.github.io/tags/ByteBuffer/"/>
    
      <category term="FileChannel" scheme="http://homoefficio.github.io/tags/FileChannel/"/>
    
      <category term="HeapByteBuffer" scheme="http://homoefficio.github.io/tags/HeapByteBuffer/"/>
    
      <category term="DirectByteBuffer" scheme="http://homoefficio.github.io/tags/DirectByteBuffer/"/>
    
      <category term="Garbage Collection" scheme="http://homoefficio.github.io/tags/Garbage-Collection/"/>
    
      <category term="OutOfMemoryError" scheme="http://homoefficio.github.io/tags/OutOfMemoryError/"/>
    
      <category term="Direct buffer memory" scheme="http://homoefficio.github.io/tags/Direct-buffer-memory/"/>
    
      <category term="BufferCache" scheme="http://homoefficio.github.io/tags/BufferCache/"/>
    
  </entry>
  
  <entry>
    <title>Spring WebFlux RequestBody</title>
    <link href="http://homoefficio.github.io/2020/08/06/Spring-WebFlux-RequestBody/"/>
    <id>http://homoefficio.github.io/2020/08/06/Spring-WebFlux-RequestBody/</id>
    <published>2020-08-05T15:54:34.000Z</published>
    <updated>2022-03-18T16:07:46.546Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Spring-WebFlux-RequestBody-Raw-vs-Mono"><a href="#Spring-WebFlux-RequestBody-Raw-vs-Mono" class="headerlink" title="Spring WebFlux RequestBody - Raw vs Mono"></a>Spring WebFlux RequestBody - Raw vs Mono</h1><p>WebFlux 사용 시 Controller 단에서 <code>RequestBody</code> 를 인자로 받을 때,</p><p>다음과 같이 <code>Mono</code>를 받아오도록 작성해야할까?</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping</span>(<span class="string">"/mono"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> Mono&lt;SellerOut&gt; <span class="title">createWithMono</span><span class="params">(@RequestBody Mono&lt;SellerIn&gt; sellerIn)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> sellerService.createWithMono(sellerIn);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>아니면 그냥 Raw 객체를 받아오도록 작성해야할까?</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@PostMapping</span>(<span class="string">"/entity"</span>)</span><br><span class="line"><span class="function"><span class="keyword">public</span> Mono&lt;SellerOut&gt; <span class="title">createWithRaw</span><span class="params">(@RequestBody SellerIn sellerIn)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> sellerService.createWithRaw(sellerIn);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>non-blocking 드라이버를 제공해주는 MongoDB를 사용한다고 가정하고 사용성과 성능 관점에서 살펴보자.</p><h2 id="사용성"><a href="#사용성" class="headerlink" title="사용성"></a>사용성</h2><p>컨트롤러 단에서는 위에서 보는 것처럼 큰 차이가 없다. 서비스가 호출하는 데이터 접근 계층(Data Access Layer)에서 작지만 큰 차이가 발생한다.</p><h3 id="ReactiveCrudRepository"><a href="#ReactiveCrudRepository" class="headerlink" title="ReactiveCrudRepository"></a>ReactiveCrudRepository</h3><p>스프링 WebFlux 환경에서도 사용하기 쉽게 추상화 된 <code>ReactiveCrudRepository</code>를 통해 쉽게 데이터 저장소에 접근할 수 있다. MongoDB 용으로 특화된 <code>ReactiveMongoRepository</code>를 사용하면 Spring Data 에서 제공해주는 편리한 <a href="https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongodb.repositories.queries" target="_blank" rel="noopener">메서드 이름 조합 방식</a>을 WebFlux에서도 사용할 수 있다. 이는 사용성과 생산성을 높여주는 데 큰 역할을 한다.</p><p>그런데 엔티티를 저장할 때 사용되는 <code>save()</code> 메서드는 다음과 같이 Mono가 아니라 Raw 객체를 사용하도록 정의돼 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@NoRepositoryBean</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ReactiveCrudRepository</span>&lt;<span class="title">T</span>, <span class="title">ID</span>&gt; <span class="keyword">extends</span> <span class="title">Repository</span>&lt;<span class="title">T</span>, <span class="title">ID</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">/**</span></span><br><span class="line"><span class="comment">   * Saves a given entity. Use the returned instance for further operations as the save operation might have changed the</span></span><br><span class="line"><span class="comment">   * entity instance completely.</span></span><br><span class="line"><span class="comment">   *</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@param</span> entity must not be &#123;<span class="doctag">@literal</span> null&#125;.</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@return</span> &#123;<span class="doctag">@link</span> Mono&#125; emitting the saved entity.</span></span><br><span class="line"><span class="comment">   * <span class="doctag">@throws</span> IllegalArgumentException in case the given &#123;<span class="doctag">@literal</span> entity&#125; is &#123;<span class="doctag">@literal</span> null&#125;.</span></span><br><span class="line"><span class="comment">   */</span></span><br><span class="line">  &lt;S extends T&gt; <span class="function">Mono&lt;S&gt; <span class="title">save</span><span class="params">(S entity)</span></span>;  <span class="comment">// Mono가 아니라 걍 S</span></span><br></pre></td></tr></table></figure><p>그래서 컨트롤러 단에서 <code>RequestBody</code>를 <code>Mono</code>로 받아오도록 만들면 <code>save()</code> 호출 전에 <code>block()</code> 같은 것을 호출해서 <code>Mono</code>에서 Raw 객체를 꺼내서 <code>save()</code>에 전달해줘야 한다. 대략 이런 식이 된다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sellerRepository.save(sellerIn.block().toEntity());</span><br></pre></td></tr></table></figure><p>쓰기도 안 좋고 보기도 안 좋고 무엇보다 중간에 <code>block()</code>을 호출하므로 <code>Mono</code>를 사용하는 의미가 퇴색된다.</p><p>정리하면 결국,</p><blockquote><p><strong><code>RequestBody</code>를 <code>Mono</code>로 받아오면 <code>ReactiveCrudRepository</code>랑 궁합이 별로다</strong></p></blockquote><h3 id="ReactiveMongoTemplate"><a href="#ReactiveMongoTemplate" class="headerlink" title="ReactiveMongoTemplate"></a>ReactiveMongoTemplate</h3><p><code>ReactiveMongoTemplate</code>에는 다음과 같이 <code>Mono</code> 타입과 Raw 객체 모두 사용해서 저장할 수 있는 API를 제공해준다. 따라서 저장은 사용성에 큰 차이가 없다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">save</span><span class="params">(Mono&lt;? extends T&gt; objectToSave)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">save</span><span class="params">(Mono&lt;? extends T&gt; objectToSave, String collectionName)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">save</span><span class="params">(T objectToSave)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> &lt;T&gt; <span class="function">Mono&lt;T&gt; <span class="title">save</span><span class="params">(T objectToSave, String collectionName)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>하지만 <code>ReactiveMongoTemplate</code>를 사용해서 조회할 때 메서드 이름 조합 방식을 사용할 수 없으므로, 아래에 보는 것처럼 <code>Query</code>를 전달해줘야 하므로 아무래도 <code>ReactiveCrudRepository</code> 보다는 사용성이 떨어진다고 할 수 있겠다.</p><p><img src="https://i.imgur.com/eogpnac.png" alt="Imgur"></p><blockquote><p><strong><code>RequestBody</code>를 <code>Mono</code>로 받아오면 <code>ReactiveMongoTemplate</code>를 사용하면 되지만, 조회할 때 <code>ReactiveCrudRepository</code>보다는 불편하다</strong></p></blockquote><h2 id="성능"><a href="#성능" class="headerlink" title="성능"></a>성능</h2><p>처음에는 예전 Servlet 관점으로 생각을 해서 <code>RequestBody</code>를 <code>Mono</code>로 받아온다 하더라도 결국은 이미 Request에서 추출해서 만들어진 Raw 객체를 <code>Mono</code>로 wrapping 해서 컨트롤러에 넣어주는 것일 거라 생각했는데 그렇지는 않은 것 같다.</p><p>WebFlux에서는 <code>HttpServletRequest</code>가 아니라 reactive 방식의 <code>ServerHttpRequest</code>를 사용한다. 따라서 Servlet 에서와는 다르게 <code>RequestBody</code>를 <code>Mono</code>로 받아오면 Raw 객체를 받아 사용하는 것과는 다르게 정말로 reactive한 처리가 가능할 수도 있겠다.</p><p>reactive 방식은 처리 속도보다는 자원 사용 관점에서의 효율성을 높이는 방식이므로 응답 속도로 비교하는 것보다는 처리량으로 비교하는 것이 합당할 것 같다. k6(<a href="https://k6.io/" target="_blank" rel="noopener">https://k6.io/</a>) 로 가상사용자 100 으로 10초간 3회 돌린 결과는 다음과 같다. 그림 위쪽이 Raw 방식, 아래쪽이 <code>Mono</code> 방식이다. <code>http_reqs</code> 항목으로 비교해보면 두 방식에서 의미있는 큰 차이는 없는 것으로 보인다.</p><p><img src="https://i.imgur.com/R3fX3fg.png" alt="Imgur"></p><p><img src="https://i.imgur.com/M8cYsQ1.png" alt="Imgur"></p><p><img src="https://i.imgur.com/VhCeVTk.png" alt="Imgur"></p><h2 id="선Raw후Mono-아니면-선Mono후Raw"><a href="#선Raw후Mono-아니면-선Mono후Raw" class="headerlink" title="선Raw후Mono? 아니면 선Mono후Raw?"></a>선Raw후Mono? 아니면 선Mono후Raw?</h2><p>Request에서 먼저 Raw 가 추출된 후에 Mono로 감싸져서 컨트롤러에 전달되는 걸까? 아니면 Request에서부터 계속 Mono(또는 Flux)인 채로 있다가 나중에 Raw가 추출되서 컨트롤러에 전달되는 걸까?</p><p>Stack을 뒤져본 결과 분기 지점은 아래와 같음을 확인했다. 하지만 여기에서 위 질문에 대한 답을 바로 얻을 수는 없었다.</p><p><img src="https://i.imgur.com/L6TuAEE.png" alt="Imgur"></p><p>조금 더 살펴본 결과 아래 그림 단계까지는 netty의 <code>NettyDataBuffer</code>로 존재하다가,</p><p><img src="https://i.imgur.com/RNY24EO.png" alt="Imgur"></p><p>아래 그림 단계에서 처음으로 Raw 값이 확인된다.</p><p><img src="https://i.imgur.com/enkERzn.png" alt="Imgur"></p><p>선Raw후Mono 인지 선Mono후Raw 인지는 아쉽게도 명확하게 알아내지 못 했다. 더 시간을 들이면 알아낼 가능성도 있지만 일단은 성능 상 어느 쪽이든 큰 차이가 없어 보이므로 이 정도에서 마무리한다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>WebFlux 사용 시 Controller 단에서 <code>RequestBody</code> 를 인자로 받을 때,</p><blockquote><ul><li><p><code>Mono</code>를 받아오도록 작성하면 <code>ReactiveCrudRepository</code>를 사용하기 불편해지고,<br>Raw 객체를 받아오도록 작성하면 <code>ReactiveCrudRepository</code>를 편하게 쓸 수 있다.</p></li><li><p><code>Mono</code>로 받아오든 Raw 객체로 받아오든 아주 특수한 상황이 아니라면 성능 상 둘 중 어느 한 쪽이 훨씬 유리하다고 할 수는 없을 것 같다.</p></li></ul></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Spring-WebFlux-RequestBody-Raw-vs-Mono&quot;&gt;&lt;a href=&quot;#Spring-WebFlux-RequestBody-Raw-vs-Mono&quot; class=&quot;headerlink&quot; title=&quot;Spring WebFlux R
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="Performance" scheme="http://homoefficio.github.io/tags/Performance/"/>
    
      <category term="Spring" scheme="http://homoefficio.github.io/tags/Spring/"/>
    
      <category term="Reactor" scheme="http://homoefficio.github.io/tags/Reactor/"/>
    
      <category term="Reactive" scheme="http://homoefficio.github.io/tags/Reactive/"/>
    
      <category term="WebFlux" scheme="http://homoefficio.github.io/tags/WebFlux/"/>
    
      <category term="RequestBody" scheme="http://homoefficio.github.io/tags/RequestBody/"/>
    
      <category term="Mono" scheme="http://homoefficio.github.io/tags/Mono/"/>
    
      <category term="K6" scheme="http://homoefficio.github.io/tags/K6/"/>
    
      <category term="ReactiveCrudRepository" scheme="http://homoefficio.github.io/tags/ReactiveCrudRepository/"/>
    
      <category term="ReactiveMongoTemplate" scheme="http://homoefficio.github.io/tags/ReactiveMongoTemplate/"/>
    
  </entry>
  
  <entry>
    <title>JPA 필요한 것만 조회하자</title>
    <link href="http://homoefficio.github.io/2020/07/23/JPA-%ED%95%84%EC%9A%94%ED%95%9C-%EA%B2%83%EB%A7%8C-%EC%A1%B0%ED%9A%8C%ED%95%98%EC%9E%90/"/>
    <id>http://homoefficio.github.io/2020/07/23/JPA-필요한-것만-조회하자/</id>
    <published>2020-07-23T06:57:12.000Z</published>
    <updated>2022-03-18T16:07:46.333Z</updated>
    
    <content type="html"><![CDATA[<h1 id="JPA-필요한-것만-조회하자"><a href="#JPA-필요한-것만-조회하자" class="headerlink" title="JPA 필요한 것만 조회하자"></a>JPA 필요한 것만 조회하자</h1><p>JPA 는 편리하지만 편리함 뒤에 숨어있는 성능 손실 위험이 있다. 이건 JPA가 그 자체로 성능 상 불리하다는 얘기가 아니라, 편하게만 쓰다보면 잘못 쓰는 길로 빠져서 성능에 해를 끼칠 위험도 꽤 있다는 얘기다.</p><p>여러가지 원칙이 있겠지만, 이번에 기억해둬야 할 원칙은 <strong>JPA는 필요한 것만 조회하자</strong></p><h2 id="엔티티"><a href="#엔티티" class="headerlink" title="엔티티"></a>엔티티</h2><p>아래는 어떤 카테고리를 나타내는 엔티티다. 카테고리는 보통 하위에 동일한 타입의 다른 카테고리를 자식으로 가지고 있고, 상위에도 동일한 타입의 다른 카테고리를 부모로 가질 수 있다. <code>parent_category_id</code> 컬럼에는 인덱스가 걸려있고, 데이터는 약 54,000건 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ZZZCategory</span> <span class="keyword">extends</span> <span class="title">BaseEntity</span> <span class="keyword">implements</span> <span class="title">TreeEntity</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="meta">@GeneratedValue</span>(strategy = GenerationType.IDENTITY)</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ManyToOne</span></span><br><span class="line">    <span class="meta">@JoinColumn</span>(name = <span class="string">"parent_category_id"</span>)</span><br><span class="line">    <span class="keyword">private</span> ZZZCategory parentCategory;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@OneToMany</span>(mappedBy = <span class="string">"parentCategory"</span>, cascade = CascadeType.PERSIST)</span><br><span class="line">    <span class="keyword">private</span> List&lt;ZZZCategory&gt; childCategories = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@OneToMany</span>(mappedBy = <span class="string">"ZZZCategory"</span>, cascade = CascadeType.PERSIST)</span><br><span class="line">    <span class="keyword">private</span> List&lt;ZZZ&gt; ZZZs = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="현재-조회-메서드"><a href="#현재-조회-메서드" class="headerlink" title="현재 조회 메서드"></a>현재 조회 메서드</h2><p>여러 개의 카테고리ID가 주어지면 주어진 카테고리ID들과 그들의 하위 카테고리ID를 모두 가져오는 로직이 필요한데 다음과 같이 구현돼 있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> Set&lt;Long&gt; <span class="title">getAllZZZCategoryIdsIncludingDescendants</span><span class="params">(Set&lt;Long&gt; ZZZCategoryIds)</span> </span>&#123;</span><br><span class="line">    List&lt;ZZZCategory&gt; ZZZCategories = ZZZCategoryRepository.findAll(ZZZCategoryIds);</span><br><span class="line">    Set&lt;Long&gt; ids = <span class="keyword">new</span> HashSet&lt;&gt;();</span><br><span class="line">    ids.addAll(ZZZCategoryIds);</span><br><span class="line">    ids.addAll(getChildIds(ZZZCategories));</span><br><span class="line">    <span class="keyword">return</span> ids;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> Set&lt;Long&gt; <span class="title">getChildIds</span><span class="params">(List&lt;ZZZCategory&gt; ZZZCategories)</span> </span>&#123;</span><br><span class="line">    Set&lt;Long&gt; ids = <span class="keyword">new</span> HashSet&lt;&gt;();</span><br><span class="line">    <span class="keyword">if</span> (ZZZCategories == <span class="keyword">null</span>)</span><br><span class="line">        <span class="keyword">return</span> ids;</span><br><span class="line"></span><br><span class="line">    ids.addAll(ZZZCategories.stream().map(ZZZCategory::getId).collect(toList()));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (ZZZCategory category : ZZZCategories) &#123;</span><br><span class="line">        List&lt;ZZZCategory&gt; children = ZZZCategoryRepository.findWithFetchedChildren(category.getId()).getChildCategories();</span><br><span class="line">        ids.addAll(getChildIds(children));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ids;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>전체 약 5.4만건에서 21개의 ID로 그 하위 ID를 모두 조회하니 약 4.8만건이 조회된다. 소요 시간은 200초 내외다.</p><p>특별할 건 없지만 필요한 건 ID 뿐인데, 편한 맛에 <code>ZZZCategoryRepository.findAll(ZZZCategoryIds)</code>를 사용해서 카테고리 통째의 목록을 읽어와서 ID 를 추출하는 방식이라는 게 눈에 살짝 거슬린다. 이걸 개선해보자.</p><h2 id="개선-후-조회-메서드"><a href="#개선-후-조회-메서드" class="headerlink" title="개선 후 조회 메서드"></a>개선 후 조회 메서드</h2><p>ZZZCategory 엔티티 통째로 조회하지 않고 ID만 조회하는 방식으로 바꿨다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> Set&lt;Long&gt; <span class="title">getAllZZZCategoryIdsIncludingDescendants</span><span class="params">(Set&lt;Long&gt; ZZZCategoryIds)</span> </span>&#123;</span><br><span class="line">    Set&lt;Long&gt; ids = <span class="keyword">new</span> HashSet&lt;&gt;();</span><br><span class="line">    ids.addAll(ZZZCategoryIds);</span><br><span class="line">    ids.addAll(getChildIds(ZZZCategoryIds));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ids;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ID를 인자로 받도록 변경</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> Set&lt;Long&gt; <span class="title">getChildIds</span><span class="params">(Set&lt;Long&gt; ZZZCategoryIds)</span> </span>&#123;</span><br><span class="line">    Set&lt;Long&gt; ids = <span class="keyword">new</span> HashSet&lt;&gt;();</span><br><span class="line">    <span class="keyword">if</span> (ZZZCategoryIds == <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> ids;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    ids.addAll(ZZZCategoryIds);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (Long ZZZCategoryId : ZZZCategoryIds) &#123;</span><br><span class="line">        <span class="comment">// ID 리스트를 반환하는 메서드로 대체</span></span><br><span class="line">        List&lt;Long&gt; childCategoryIds = ZZZCategoryRepository.findChildCategoryIds(ZZZCategoryId);</span><br><span class="line">        ids.addAll(getChildIds(<span class="keyword">new</span> HashSet&lt;&gt;(childCategoryIds)));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ids;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>엔티티를 통째로 가져오는 <code>ZZZCategoryRepository.findAll(ZZZCategoryIds)</code> 대신에 ID만 가져오는 메서드를 QueryDSL로 구현해서 대신 사용했다. 앞에서 얘기한 것처럼 <code>parent_category_id</code>에는 인덱스가 걸려 있으므로 조회 효율이 괜찮을 것이다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> List&lt;Long&gt; <span class="title">findChildCategoryIds</span><span class="params">(Long parentId)</span> </span>&#123;</span><br><span class="line">    QZZZCategory zzzCategory = QZZZCategory.zzzCategory;</span><br><span class="line">    <span class="keyword">return</span> from(zzzCategory)</span><br><span class="line">            .where(zzzCategory.parentCategory.id.eq(parentId))</span><br><span class="line">            .select(zzzCategory.id)</span><br><span class="line">            .fetch();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>개선 후 실행해보니 똑같은 조회를 수행하는데 소요 시간이 <strong>200초 내외에서 25초 내외로 개선되어 약 8배 정도 빨라졌다.</strong></p><p>사실 위 엔티티 코드는 간단한 설명을 위해 실제보다 축약한 형태라서 위 코드만으로는 8배 까지 개선되지는 않을 수도 있다.</p><p>여튼 중요한 것은 <strong>JPA를 쓸 때는 편하다고 엔티티를 통째로 다 읽어들이지 말고, 조금 손이 가더라도 필요한 것만 골라서 조회하자.</strong></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;JPA-필요한-것만-조회하자&quot;&gt;&lt;a href=&quot;#JPA-필요한-것만-조회하자&quot; class=&quot;headerlink&quot; title=&quot;JPA 필요한 것만 조회하자&quot;&gt;&lt;/a&gt;JPA 필요한 것만 조회하자&lt;/h1&gt;&lt;p&gt;JPA 는 편리하지만 편리함 뒤에
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="JPA" scheme="http://homoefficio.github.io/tags/JPA/"/>
    
      <category term="QueryDSL" scheme="http://homoefficio.github.io/tags/QueryDSL/"/>
    
      <category term="Projection" scheme="http://homoefficio.github.io/tags/Projection/"/>
    
      <category term="Performance" scheme="http://homoefficio.github.io/tags/Performance/"/>
    
  </entry>
  
  <entry>
    <title>IDE 에서는 되는데 jar 에서는 안 돼요 - Java Resource</title>
    <link href="http://homoefficio.github.io/2020/07/21/IDE-%EC%97%90%EC%84%9C%EB%8A%94-%EB%90%98%EB%8A%94%EB%8D%B0-jar-%EC%97%90%EC%84%9C%EB%8A%94-%EC%95%88-%EB%8F%BC%EC%9A%94-Java-Resource/"/>
    <id>http://homoefficio.github.io/2020/07/21/IDE-에서는-되는데-jar-에서는-안-돼요-Java-Resource/</id>
    <published>2020-07-21T13:55:37.000Z</published>
    <updated>2022-03-18T16:07:46.292Z</updated>
    
    <content type="html"><![CDATA[<h1 id="IDE-에서는-되는데-jar-에서는-안-돼요-Java-Resource"><a href="#IDE-에서는-되는데-jar-에서는-안-돼요-Java-Resource" class="headerlink" title="IDE 에서는 되는데 jar 에서는 안 돼요 - Java Resource"></a>IDE 에서는 되는데 jar 에서는 안 돼요 - Java Resource</h1><blockquote><p>한 줄 요약: <strong>웬만하면 <code>getResource()</code> 쓰지 말고 <code>getResourceAsStream()</code> 쓰자</strong></p></blockquote><h2 id="기본-폴더-구조"><a href="#기본-폴더-구조" class="headerlink" title="기본 폴더 구조"></a>기본 폴더 구조</h2><p>자바에서는 메이븐이 널리 사용되면서 아래와 같은 폴더 구조가 표준처럼 사용되고 있다.</p><p><img src="https://i.imgur.com/zpDIgeL.png" alt="Imgur"></p><p>src/main/java 폴더 하위에 있는 java 파일은 빌드 후 target/classes 하위에 위치하게 되고,<br>src/main/resources/static 폴더는 빌드 후 target/static 폴더 바로 아래에 위치하게 된다.</p><p>자바 파일이든 그 외 파일이든 결국 빌드 후에는 target 디렉터리가 루트 디렉터리가 된다.</p><h2 id="main-클래스"><a href="#main-클래스" class="headerlink" title="main 클래스"></a>main 클래스</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf</span>4j</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">App</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        ResourceLoader resourceLoader =</span><br><span class="line">                <span class="keyword">new</span> ResourceLoader(<span class="string">"/static"</span>, Path.of(<span class="string">"/static"</span>));</span><br><span class="line">        resourceLoader.loadResourceAsFile(<span class="string">"/folder1/sample1"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="getResource"><a href="#getResource" class="headerlink" title="getResource()"></a>getResource()</h2><p><code>getResource()</code>를 사용해서 파일로 읽어들인 후 출력하는 프로그램은 다음과 같다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf</span>4j</span><br><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ResourceLoader</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String root;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Path rootPath;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadResourceAsFile</span><span class="params">(String resourceLocation)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        log.info(<span class="string">"*** getResource() + File 방식"</span>);</span><br><span class="line">        log.info(<span class="string">"content root: &#123;&#125;"</span>, rootPath);</span><br><span class="line">        log.info(<span class="string">"resourceLocation: &#123;&#125;"</span>, resourceLocation);</span><br><span class="line"></span><br><span class="line">        URL resourceURL = <span class="keyword">this</span>.getClass().getResource(root + resourceLocation);</span><br><span class="line">        log.info(<span class="string">"resourceURL: &#123;&#125;"</span>, resourceURL);</span><br><span class="line"></span><br><span class="line">        String fileLocation = resourceURL.getFile();</span><br><span class="line">        log.info(<span class="string">"fileLocation from URL: &#123;&#125;"</span>, fileLocation);</span><br><span class="line"></span><br><span class="line">        File file = <span class="keyword">new</span> File(fileLocation);</span><br><span class="line">        FileReader fileReader = <span class="keyword">new</span> FileReader(file);</span><br><span class="line">        <span class="keyword">char</span>[] chars = <span class="keyword">new</span> <span class="keyword">char</span>[(<span class="keyword">int</span>) file.length()];</span><br><span class="line">        fileReader.read(chars);</span><br><span class="line"></span><br><span class="line">        log.info(<span class="string">"resource contents: &#123;&#125;"</span>, <span class="keyword">new</span> String(chars));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="IDE-에서-동작-확인"><a href="#IDE-에서-동작-확인" class="headerlink" title="IDE 에서 동작 확인"></a>IDE 에서 동작 확인</h3><p>IDE 에서 실행하면 다음과 같이 정상적으로 출력된다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">00:56:15.152 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + File 방식</span><br><span class="line">00:56:15.155 [main] INFO io.homo_efficio.ResourceLoader - content root: /static</span><br><span class="line">00:56:15.156 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1</span><br><span class="line">00:56:15.158 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/static/folder1/sample1</span><br><span class="line">00:56:15.158 [main] INFO io.homo_efficio.ResourceLoader - fileLocation from URL: /Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/static/folder1/sample1</span><br><span class="line">00:56:15.159 [main] INFO io.homo_efficio.ResourceLoader - resource contents: Sample File 1</span><br></pre></td></tr></table></figure><p><strong>resourceURL 값이 <code>file:</code> 로 시작한다는 것을 기억해두자.</strong></p><p>읽을 파일 경로는 <code>/Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/static/folder1/sample1</code>로 표시되는데 이는 파일시스템에 실제 존재하는 경로와 일치한다.</p><h3 id="fat-jar-에서-동작-확인"><a href="#fat-jar-에서-동작-확인" class="headerlink" title="fat-jar 에서 동작 확인"></a>fat-jar 에서 동작 확인</h3><p>하지만 다음과 같이 <code>java -jar</code> 명령으로 fat-jar 파일을 실행하면 다음과 같이 오류가 발생한다. maven에서 fat-jar 만드는 방법은 <a href="https://github.com/HomoEfficio/dev-tips/blob/master/Maven-fat-jar.md" target="_blank" rel="noopener">https://github.com/HomoEfficio/dev-tips/blob/master/Maven-fat-jar.md</a> 를 참고한다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">maven-fat-jar-test git:master 🍺🦑🍺🍕🍺 ❯ java -jar target/maven-fat-jar.jar</span><br><span class="line">00:58:11.479 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + File 방식</span><br><span class="line">00:58:11.481 [main] INFO io.homo_efficio.ResourceLoader - content root: /static</span><br><span class="line">00:58:11.482 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1</span><br><span class="line">00:58:11.484 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: jar:file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1</span><br><span class="line">00:58:11.484 [main] INFO io.homo_efficio.ResourceLoader - fileLocation from URL: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1</span><br><span class="line">Exception in thread &quot;main&quot; java.io.FileNotFoundException: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1 (No such file or directory)</span><br><span class="line">        at java.base/java.io.FileInputStream.open0(Native Method)</span><br><span class="line">        at java.base/java.io.FileInputStream.open(FileInputStream.java:212)</span><br><span class="line">        at java.base/java.io.FileInputStream.&lt;init&gt;(FileInputStream.java:154)</span><br><span class="line">        at java.base/java.io.FileReader.&lt;init&gt;(FileReader.java:75)</span><br><span class="line">        at io.homo_efficio.ResourceLoader.loadResourceAsFile(ResourceLoader.java:38)</span><br><span class="line">        at io.homo_efficio.App.main(App.java:19)</span><br></pre></td></tr></table></figure><p>에러 메시지를 보면 파일 경로(정확히는 URL)가 <code>file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/sample1</code> 라고 표시된다. fat-jar 파일이 중간에 <code>mavan-fat-jar.jar!</code> 로 표시돼 있는데 이렇게 <code>!</code>가 포함된 경로는 실제로 존재하지 않기 때문에 위와 같은 에러가 발생하게 된다.</p><p>즉 IDE에서 실행할 때는 실제 파일시스템 기준 경로를 따르므로 에러가 발생하지 않지만, jar 파일을 읽을 때는 jar 파일이 <code>!</code>와 함께 표시되기 때문에 실제 파일시스템 경로에 맞지 않아 에러가 발생한다.</p><p><strong>rsourceURL 값이 IDE 에서 실행할 때는 <code>file:</code> 로 시작했는데, jar 로 실행할 때는 <code>jar:file:</code>로 시작한다.</strong> 이것도 기억해두자.</p><p>어쨌든 자바 프로그램은 실제로는 대부분 jar 로 만들어져서 실행될텐데, jar 에서 제대로 실행이 안 된다면 이를 어쩐다?</p><h2 id="getResourceAsStream"><a href="#getResourceAsStream" class="headerlink" title="getResourceAsStream()"></a>getResourceAsStream()</h2><p><code>getResource()</code> 는 기본적으로 URL 을 반환한다. URL은 위와 같이 jar 파일을 <code>!</code>와 함께 표시하기 때문에, jar 실행 시 에러가 발생한다.</p><p>하지만 <code>getResourceAsStream()</code>은 InputStream 을 반환한다. 그리고 Java 9 에서 추가된 <code>InputStream.readAllBytes()</code>를 사용하면 편리하게 InputStream 을 읽어서 byte[] 에 저장할 수 있다.(물론 대용량 데이터를 <code>readAllBytes()</code>로 읽어들이면 망함. 대용량 파일 처리는 <a href="https://homoefficio.github.io/2019/02/27/Java-NIO-Direct-Buffer를-이용해서-대용량-파일-행-기준으로-쪼개기/">https://homoefficio.github.io/2019/02/27/Java-NIO-Direct-Buffer를-이용해서-대용량-파일-행-기준으로-쪼개기/</a> 를 참고하자)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ResourceLoader.java</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadResourceAsStream</span><span class="params">(String resourceLocation)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        log.info(<span class="string">"OOO getResourceAsStream() 방식"</span>);</span><br><span class="line">        log.info(<span class="string">"content root: &#123;&#125;"</span>, root);</span><br><span class="line">        log.info(<span class="string">"resourceLocation: &#123;&#125;"</span>, resourceLocation);</span><br><span class="line"></span><br><span class="line">        InputStream resourceAsStream = <span class="keyword">this</span>.getClass().getResourceAsStream(root + resourceLocation);</span><br><span class="line">        <span class="keyword">byte</span>[] bytes = resourceAsStream.readAllBytes();</span><br><span class="line">        log.info(<span class="string">"resource contents: &#123;&#125;"</span>, <span class="keyword">new</span> String(bytes, StandardCharsets.UTF_8));</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h3 id="IDE-에서-동작-확인-1"><a href="#IDE-에서-동작-확인-1" class="headerlink" title="IDE 에서 동작 확인"></a>IDE 에서 동작 확인</h3><p>IDE 에서 실행하면 다음과 같이 정상적으로 실행된다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - OOO getResourceAsStream() 방식</span><br><span class="line">23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - content root: /static</span><br><span class="line">23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1</span><br><span class="line">23:55:45.443 [main] INFO io.homo_efficio.ResourceLoader - resource contents: Sample File 1</span><br></pre></td></tr></table></figure><h3 id="fat-jar-에서-동작-확인-1"><a href="#fat-jar-에서-동작-확인-1" class="headerlink" title="fat-jar 에서 동작 확인"></a>fat-jar 에서 동작 확인</h3><p>fat-jar 실행 시에도 정상적으로 실행된다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">maven-fat-jar-test git:master 🍺🦑🍺🍕🍺 ❯ java -jar target/maven-fat-jar.jar</span><br><span class="line">00:01:31.774 [main] INFO io.homo_efficio.ResourceLoader - OOO getResourceAsStream() 방식</span><br><span class="line">00:01:31.775 [main] INFO io.homo_efficio.ResourceLoader - content root: /static</span><br><span class="line">00:01:31.776 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/sample1</span><br><span class="line">00:01:31.778 [main] INFO io.homo_efficio.ResourceLoader - resource contents: Sample File 1</span><br></pre></td></tr></table></figure><p>따라서 <code>getResource()</code> 보다는 <code>getResourceAsStream()</code>을 사용하자. 끝.</p><h2 id="속사정"><a href="#속사정" class="headerlink" title="속사정"></a>속사정</h2><p>혹시 왜 이런 차이가 발생하는지 궁금한 사람들은 이어서 읽어보자.</p><p><code>getResourceAsStream()</code> 호출을 따라가보면 Java 14 기준 <code>BuiltinClassLoader</code> 클래스에서 아래와 같은 코드를 만나게 되는데,</p><p><img src="https://i.imgur.com/m87zAsl.png" alt="Imgur"></p><p><code>openStream()</code> 을 따라가면 왜 되는지 알 수 있다. <strong><code>openConnection()</code>은 URLConnection 을 반환하는데, 이 URLConnection 에는 여러가지 SubClass가 있어서 다형적으로 동작할 수 있다.</strong></p><p><img src="https://i.imgur.com/oZz9Wu2.png" alt="Imgur"></p><p>앞에서 IDE 에서 실행할 때는 URL 값이 <code>file:</code> 로 시작하고, jar 로 실행할 떄는 URL 값이 <code>jar:file:</code> 로 시작하는 것을 기억해두자고 한 것을 상기해보면 답이 보일 것이다.</p><p><strong>URL 이 <code>file:</code> 로 시작하는 IDE 에서는 FileURLConnection 이 사용되고, URL 이 <code>jar:file:</code> 로 시작하는 jar 실행에서는 JarURLConnection 이 사용된다. <code>getResourceAsStream()</code>은 다형적으로 동작하도록 구현돼 있어서 두 상황 모두에서 잘 동작할 수 있다.</strong></p><p>그럼 <code>getResource()</code>는?</p><p><strong>사실 문제는 <code>getResource()</code> 가 아니라 <code>getResource()</code> 이 반환하는 URL 을 어떻게 쓰느냐에 있다.</strong> 똑같이 <code>getResource()</code> 를 사용하더라도 다음과 같이 <code>openStream()</code> 을 사용하면 <code>getResource()</code> 을 사용해도 jar 에서도 잘 동작한다.</p><p>즉, <strong>URL 에서 File 을 생성하면 다형성이 적용되지 않아 IDE 에서는 되지만 jar 에서는 안 되는 상황이 연출</strong>되고,<br><strong>URL 에서 InputStream 을 뽑아서 사용하면 다형성이 적용돼서 IDE, jar 모두에서 잘 동작한다.</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ResourceLoader.java</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadResourceAsFile</span><span class="params">(String resourceLocation)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">        log.info(<span class="string">"*** getResource() + File 방식"</span>);</span><br><span class="line">        log.info(<span class="string">"content root: &#123;&#125;"</span>, rootPath);</span><br><span class="line">        log.info(<span class="string">"resourceLocation: &#123;&#125;"</span>, resourceLocation);</span><br><span class="line"></span><br><span class="line">        URL resourceURL = <span class="keyword">this</span>.getClass().getResource(root + resourceLocation);</span><br><span class="line">        log.info(<span class="string">"resourceURL: &#123;&#125;"</span>, resourceURL);</span><br><span class="line"></span><br><span class="line"><span class="comment">//        String fileLocation = resourceURL.getFile();</span></span><br><span class="line"><span class="comment">//        log.info("fileLocation from URL: &#123;&#125;", fileLocation);</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">//        File file = new File(fileLocation);</span></span><br><span class="line"><span class="comment">//        FileReader fileReader = new FileReader(file);</span></span><br><span class="line"><span class="comment">//        char[] chars = new char[(int) file.length()];</span></span><br><span class="line"><span class="comment">//        fileReader.read(chars);</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">//        log.info("resource contents: &#123;&#125;", new String(chars));</span></span><br><span class="line"></span><br><span class="line">        InputStream inputStream = resourceURL.openStream();</span><br><span class="line">        <span class="keyword">byte</span>[] bytes = inputStream.readAllBytes();</span><br><span class="line">        log.info(<span class="string">"resource contents: &#123;&#125;"</span>, <span class="keyword">new</span> String(bytes, StandardCharsets.UTF_8));</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="Jackson"><a href="#Jackson" class="headerlink" title="Jackson"></a>Jackson</h2><p>자바에서 JSON 처리에 널리 사용되는 Jackson 은 어떨까?</p><p>다음과 같이 URL 을 <code>readValue()</code> 메서드에 인자로 넘겨주는 방식으로 구현하면 IDE, jar 모두에서 잘 동작한다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ResourceLoader.java</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadConfig</span><span class="params">(String resourceLocation)</span> </span>&#123;</span><br><span class="line">        log.info(<span class="string">"*** getResource() + Jackson 방식"</span>);</span><br><span class="line">        log.info(<span class="string">"content root: &#123;&#125;"</span>, rootPath);</span><br><span class="line">        log.info(<span class="string">"resourceLocation: &#123;&#125;"</span>, resourceLocation);</span><br><span class="line"></span><br><span class="line">        URL configURL = <span class="keyword">this</span>.getClass().getResource(root + resourceLocation);</span><br><span class="line">        log.info(<span class="string">"resourceURL: &#123;&#125;"</span>, configURL);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            ObjectMapper objectMapper = <span class="keyword">new</span> ObjectMapper();</span><br><span class="line">            Config config = objectMapper.readValue(configURL, Config.class);</span><br><span class="line">            log.info(<span class="string">"title in config: &#123;&#125;"</span>, config.getTitle());</span><br><span class="line">            log.info(<span class="string">"tags in config: [&#123;&#125;]"</span>, String.join(<span class="string">", "</span>, config.getTags()));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">"설정 파일 로딩에 실패했습니다."</span>, e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>IDE 실행 결과</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">12:09:01.465 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + Jackson 방식</span><br><span class="line">12:09:01.465 [main] INFO io.homo_efficio.ResourceLoader - content root: /static</span><br><span class="line">12:09:01.465 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/config.json</span><br><span class="line">12:09:01.466 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/static/folder1/config.json</span><br><span class="line">12:09:01.617 [main] INFO io.homo_efficio.ResourceLoader - title in config: Java Resource Handling</span><br><span class="line">12:09:01.617 [main] INFO io.homo_efficio.ResourceLoader - tags in config: [Java, Resource, fat, jar]</span><br></pre></td></tr></table></figure><p>jar 실행 결과</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">maven-fat-jar-test git:master 🍺🦑🍺🍕🍺 ❯ java -jar target/maven-fat-jar.jar</span><br><span class="line">12:09:25.075 [main] INFO io.homo_efficio.ResourceLoader - *** getResource() + Jackson 방식</span><br><span class="line">12:09:25.075 [main] INFO io.homo_efficio.ResourceLoader - content root: /static</span><br><span class="line">12:09:25.075 [main] INFO io.homo_efficio.ResourceLoader - resourceLocation: /folder1/config.json</span><br><span class="line">12:09:25.076 [main] INFO io.homo_efficio.ResourceLoader - resourceURL: jar:file:/Users/1003604/gitRepo/study/maven-fat-jar-test/target/maven-fat-jar.jar!/static/folder1/config.json</span><br><span class="line">12:09:25.213 [main] INFO io.homo_efficio.ResourceLoader - title in config: Java Resource Handling</span><br><span class="line">12:09:25.213 [main] INFO io.homo_efficio.ResourceLoader - tags in config: [Java, Resource, fat, jar]</span><br></pre></td></tr></table></figure><h2 id="Properties"><a href="#Properties" class="headerlink" title="Properties"></a>Properties</h2><p><code>.properties</code> 파일을 읽을 때 사용하는 <code>Properties</code> 클래스에는 <code>load(Reader r)</code>, <code>load(InputStream i)</code> 두 가지 메서드가 있다. IDE, jar 모두에서 동작하려면 어느 것을 써야할지 이젠 해보지 않아도 알 수 있을 것 같다.</p><h2 id="디렉터리-내-파일-목록"><a href="#디렉터리-내-파일-목록" class="headerlink" title="디렉터리 내 파일 목록"></a>디렉터리 내 파일 목록</h2><p>개별 파일은 위와 같이 대응할 수 있다는 걸 알게 됐다. 그런데 디렉터리 내 파일 목록을 읽어서 원하는 대로 처리하는 것도 IDE, jar 에서 모두 가능할까?</p><p>이미 구구절절 많이 떠들었으니 바로 코드로 살펴보자. <strong>jar 파일 내에서 목록 단위로 처리하려면 <code>JarFile</code>이 필요하다는 점만 기억해두자.</strong> 나머지 주의해서 볼 점은 주석에 표시해놨다.</p><p>마지막에 나오는 코드 <code>enumerationAsStream()</code> 메서드는 <code>Enumeration</code>을 copy를 유발하지 않고 Stream 으로 쓸 수 있게 해주는 유틸 메서드다.(자바는 왜 이런 걸 공식 SDK에 포함하지 않는 건가..)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadDirectoryAsStream</span><span class="params">(String dir)</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">    log.info(<span class="string">"OOO getResourceAsStream() + Directory"</span>);</span><br><span class="line">    log.info(<span class="string">"content root: &#123;&#125;"</span>, root);</span><br><span class="line">    log.info(<span class="string">"dir: &#123;&#125;"</span>, dir);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// IDE 에서는 잘 동작, jar 에서는 에러는 발생하지 않으나 esourceAsStream.readAllBytes() 값이 비어있음</span></span><br><span class="line">    log.info(<span class="string">"USING naive getResourceAsStream(String directory) -----"</span>);</span><br><span class="line">    InputStream resourceAsStream = <span class="keyword">this</span>.getClass().getResourceAsStream(root + dir);</span><br><span class="line">    <span class="keyword">byte</span>[] bytes = resourceAsStream.readAllBytes();</span><br><span class="line">    log.info(<span class="string">"resource contents length: &#123;&#125;"</span>, bytes.length);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (bytes.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        log.info(<span class="string">"resource contents: &#123;&#125;"</span>, <span class="keyword">new</span> String(bytes, StandardCharsets.UTF_8));</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// jar 에서는 잘 동작</span></span><br><span class="line">        <span class="comment">// IDE 에서는 예외 발생: java.io.FileNotFoundException: /Users/1003604/gitRepo/study/maven-fat-jar-test/target/classes/io/homo_efficio (Is a directory)</span></span><br><span class="line">        <span class="comment">// 따라서 bytes.length = 0 일 때만 실행하도록</span></span><br><span class="line">        log.info(<span class="string">"USING JarFile -----"</span>);</span><br><span class="line">        String path = <span class="keyword">this</span>.getClass().getResource(<span class="string">""</span>).getPath();</span><br><span class="line">        log.info(<span class="string">"resourcePath: &#123;&#125;"</span>, path);</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">int</span> exclamationIndex = path.lastIndexOf(<span class="string">"!"</span>) &gt; <span class="number">0</span> ? path.lastIndexOf(<span class="string">"!"</span>) : path.length();</span><br><span class="line">        String jarFilePath = path.substring(<span class="number">0</span>, exclamationIndex).replaceAll(<span class="string">"file:"</span>, <span class="string">""</span>);</span><br><span class="line">        log.info(<span class="string">"jarFilePath : &#123;&#125;"</span>, jarFilePath);</span><br><span class="line">        </span><br><span class="line">        LocalDateTime start = LocalDateTime.now();</span><br><span class="line">        log.info(<span class="string">"jarFile start: &#123;&#125;"</span>, start);</span><br><span class="line">        JarFile jarFile = <span class="keyword">new</span> JarFile(jarFilePath);</span><br><span class="line">        LocalDateTime end = LocalDateTime.now();</span><br><span class="line">        log.info(<span class="string">"jarFile end  : &#123;&#125;"</span>, end);</span><br><span class="line">        </span><br><span class="line">        enumerationAsStream(jarFile.entries())</span><br><span class="line">                .filter(entry -&gt; entry.getRealName().startsWith((root + dir).substring(<span class="number">1</span>)))</span><br><span class="line">                .forEach(entry -&gt; log.info(<span class="string">"jarEntry: &#123;&#125;"</span>, entry.getRealName()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// From https://stackoverflow.com/a/23276455</span></span><br><span class="line"><span class="keyword">static</span> &lt;T&gt; <span class="function">Stream&lt;T&gt; <span class="title">enumerationAsStream</span><span class="params">(Enumeration&lt;T&gt; e)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> StreamSupport.stream(</span><br><span class="line">            Spliterators.spliteratorUnknownSize(</span><br><span class="line">                    <span class="keyword">new</span> Iterator&lt;T&gt;() &#123;</span><br><span class="line">                        <span class="meta">@Override</span></span><br><span class="line">                        <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">hasNext</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                            <span class="keyword">return</span> e.hasMoreElements();</span><br><span class="line">                        &#125;</span><br><span class="line"></span><br><span class="line">                        <span class="meta">@Override</span></span><br><span class="line">                        <span class="function"><span class="keyword">public</span> T <span class="title">next</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                            <span class="keyword">return</span> e.nextElement();</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;,</span><br><span class="line">                    Spliterator.ORDERED</span><br><span class="line">            ), <span class="keyword">false</span></span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;IDE-에서는-되는데-jar-에서는-안-돼요-Java-Resource&quot;&gt;&lt;a href=&quot;#IDE-에서는-되는데-jar-에서는-안-돼요-Java-Resource&quot; class=&quot;headerlink&quot; title=&quot;IDE 에서는 되는데 jar 
      
    
    </summary>
    
      <category term="Technique" scheme="http://homoefficio.github.io/categories/Technique/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="URL" scheme="http://homoefficio.github.io/tags/URL/"/>
    
      <category term="Resource" scheme="http://homoefficio.github.io/tags/Resource/"/>
    
      <category term="getResource" scheme="http://homoefficio.github.io/tags/getResource/"/>
    
      <category term="getResourceAsStream" scheme="http://homoefficio.github.io/tags/getResourceAsStream/"/>
    
      <category term="maven" scheme="http://homoefficio.github.io/tags/maven/"/>
    
      <category term="fat-jar" scheme="http://homoefficio.github.io/tags/fat-jar/"/>
    
      <category term="jar" scheme="http://homoefficio.github.io/tags/jar/"/>
    
      <category term="file" scheme="http://homoefficio.github.io/tags/file/"/>
    
      <category term="loading" scheme="http://homoefficio.github.io/tags/loading/"/>
    
  </entry>
  
  <entry>
    <title>Java Memory Monitoring</title>
    <link href="http://homoefficio.github.io/2020/04/09/Java-Memory-Monitoring/"/>
    <id>http://homoefficio.github.io/2020/04/09/Java-Memory-Monitoring/</id>
    <published>2020-04-09T07:57:33.000Z</published>
    <updated>2022-03-18T16:07:46.353Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Java-메모리-모니터링"><a href="#Java-메모리-모니터링" class="headerlink" title="Java 메모리 모니터링"></a>Java 메모리 모니터링</h1><p>두서 없이 이것저것 모아본 자바 메모리 모니터링</p><h1 id="Heap-Dump"><a href="#Heap-Dump" class="headerlink" title="Heap Dump"></a>Heap Dump</h1><blockquote><p>힙에 있는 모든 객체 Dump<br>jmap -dump:format=b,file=HEAP_DUMP_OUTPUT_FILE_NAME.hprof PID</p><p>힙에 있는 Live 객체만 Dump<br>jmap -dump:live,format=b,file=HEAP_DUMP_OUTPUT_FILE_NAME.hprof PID</p></blockquote><h1 id="힙-분석-도구"><a href="#힙-분석-도구" class="headerlink" title="힙 분석 도구"></a>힙 분석 도구</h1><p>jmap 으로 생성한 Heap Dump 파일을 열어서 분석할 수 있는 도구</p><ul><li>Eclipse MAT: <a href="https://www.eclipse.org/mat/" target="_blank" rel="noopener">https://www.eclipse.org/mat/</a><ul><li>메모리 누수 의심 내역을 보고서 형태로 보여줘서 좋음</li><li><img src="https://i.imgur.com/zeiXYxF.png" alt="Imgur"></li><li><img src="https://i.imgur.com/yEa9Xpk.png" alt="Imgur"></li><li><img src="https://i.imgur.com/xiTL9Lh.png" alt="Imgur"></li><li>Shallow Heap vs Retained Heap: <a href="https://dzone.com/articles/eclipse-mat-shallow-heap-retained-heap" target="_blank" rel="noopener">https://dzone.com/articles/eclipse-mat-shallow-heap-retained-heap</a><ul><li>shallow heap은 한 객체만이 점유한 힙의 크기</li><li>retained heap은 한 객체가 제거될 때 함께 제거될 수 있는 객체들이 점유하고 있는 힙의 크기</li></ul></li></ul></li><li>VisuamVM: <a href="https://visualvm.github.io/" target="_blank" rel="noopener">https://visualvm.github.io/</a></li></ul><h1 id="메모리-사용-및-GC-모니터링"><a href="#메모리-사용-및-GC-모니터링" class="headerlink" title="메모리 사용 및 GC 모니터링"></a>메모리 사용 및 GC 모니터링</h1><blockquote><p>jstat -gc -h10 -t PID 10000<br>10줄마다 헤더 출력, 타임스탬프 출력, 10000밀리초마다 출력</p></blockquote><p><img src="https://i.imgur.com/d0TMbpm.png" alt="Imgur"></p><h1 id="Thread-Dump"><a href="#Thread-Dump" class="headerlink" title="Thread Dump"></a>Thread Dump</h1><blockquote><p>jstack PID &gt; THREAD_DUMP_OUTPUT_FILENAME</p></blockquote><h1 id="Thread-Dump-분석-사이트"><a href="#Thread-Dump-분석-사이트" class="headerlink" title="Thread Dump 분석 사이트"></a>Thread Dump 분석 사이트</h1><ul><li><a href="https://fastthread.io" target="_blank" rel="noopener">https://fastthread.io</a></li></ul><h1 id="자바-메모리-분석-관련-좋은-자료"><a href="#자바-메모리-분석-관련-좋은-자료" class="headerlink" title="자바 메모리 분석 관련 좋은 자료"></a>자바 메모리 분석 관련 좋은 자료</h1><ul><li>GC 관련: <ul><li><a href="https://d2.naver.com/helloworld/6043" target="_blank" rel="noopener">https://d2.naver.com/helloworld/6043</a></li></ul></li><li>OOM 및 Heap Shrinkage 관련: <ul><li><a href="https://www.samsungsds.com/global/ko/support/insights/1209174_2284.html" target="_blank" rel="noopener">https://www.samsungsds.com/global/ko/support/insights/1209174_2284.html</a></li></ul></li><li>메모리 누수 관련: <ul><li><a href="https://d2.naver.com/helloworld/1326256" target="_blank" rel="noopener">https://d2.naver.com/helloworld/1326256</a></li><li><a href="https://woowabros.github.io/tools/2019/05/24/jvm_memory_leak.html" target="_blank" rel="noopener">https://woowabros.github.io/tools/2019/05/24/jvm_memory_leak.html</a></li></ul></li><li>Thread Dump 관련:<ul><li><a href="https://brunch.co.kr/@springboot/126" target="_blank" rel="noopener">https://brunch.co.kr/@springboot/126</a></li><li><a href="https://d2.naver.com/helloworld/10963" target="_blank" rel="noopener">https://d2.naver.com/helloworld/10963</a></li></ul></li></ul><h1 id="짤막-지식"><a href="#짤막-지식" class="headerlink" title="짤막 지식"></a>짤막 지식</h1><h2 id="Heap-Shrinkage"><a href="#Heap-Shrinkage" class="headerlink" title="Heap Shrinkage"></a>Heap Shrinkage</h2><ul><li><strong>메모리 사용량을 증가 시키는 작업이 끝난다고 해서 메모리 사용량이 바로 줄어들지는 않음</strong><ul><li><strong>어떤 작업 완료 후 top로 확인한 메모리 사용이 줄지 않는다고 해서 무조건 메모리 누수라고 판단하면 안됨</strong></li></ul></li></ul><h3 id="사례"><a href="#사례" class="headerlink" title="사례"></a>사례</h3><ul><li>작업 시작 전<ul><li><img src="https://i.imgur.com/oZV4Eat.png" alt="Imgur"></li></ul></li><li>작업 중 - 메모리 사용량 증가<ul><li><img src="https://i.imgur.com/t8hwQHz.png" alt="Imgur"></li></ul></li><li>작업 완료 후 - 메모리 사용량 감소하지 않음<ul><li><img src="https://i.imgur.com/sGBCLbK.png" alt="Imgur"></li></ul></li><li>메모리 회수 - jmap 자체가 메모리 회수 명령은 아니지만, 부수적으로 메모리 사용량이 감소함<ul><li><img src="https://i.imgur.com/gQze3QN.png" alt="Imgur"></li></ul></li><li>따라서 jmap 을 힙 덤프 뿐아니라 메모리 누수 확인 간편법으로 사용할 수도 있음<ul><li>반드시 <code>-dump:live</code> 옵션을 줘야 과도한 크기의 힙 덤프 파일이 생성되지 않음</li><li>jmap 을 해도 메모리 사용량이 많이 줄지 않는다면 메모리 누수가 있는 걸로 추정 가능</li></ul></li></ul><h2 id="경험상-중첩된-Collection은-메모리-이슈-발생할-낌새가-좀-있더라"><a href="#경험상-중첩된-Collection은-메모리-이슈-발생할-낌새가-좀-있더라" class="headerlink" title="경험상 중첩된 Collection은 메모리 이슈 발생할 낌새가 좀 있더라.."></a>경험상 중첩된 Collection은 메모리 이슈 발생할 낌새가 좀 있더라..</h2><ul><li>List의 원소 안에 또 List 가 있고 그 안에 또 List 가 있고..</li><li>Map 의 value 중에 또 Map 이 있고 그 안에 또 Map 이 있고..</li><li>대량의 데이터가 위와 같은 형태로 처리되면 메모리 회수 타이밍이 좀 지연되는 것 같고, 결국 메모리 사용량이 과도하게 높아져서 OOM 이 발생하기도 하더라..</li><li>이럴 때는 GC에 맡기지 말고 중간중간 Collection 안에 중첩된 Collection 들을 직접 <code>clear()</code> 해주는 게 도움이 될 수도..</li><li>JPA 에서의 연관관계로 중첩된 Collection이 생겨난 경우에는 LazyLoading을 통해 OOM 을 방지할 수도 있다</li></ul>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Java-메모리-모니터링&quot;&gt;&lt;a href=&quot;#Java-메모리-모니터링&quot; class=&quot;headerlink&quot; title=&quot;Java 메모리 모니터링&quot;&gt;&lt;/a&gt;Java 메모리 모니터링&lt;/h1&gt;&lt;p&gt;두서 없이 이것저것 모아본 자바 메모리 모니터링
      
    
    </summary>
    
      <category term="Performance" scheme="http://homoefficio.github.io/categories/Performance/"/>
    
    
      <category term="Java" scheme="http://homoefficio.github.io/tags/Java/"/>
    
      <category term="Memory" scheme="http://homoefficio.github.io/tags/Memory/"/>
    
      <category term="Monitoring" scheme="http://homoefficio.github.io/tags/Monitoring/"/>
    
      <category term="Eclipse MAT" scheme="http://homoefficio.github.io/tags/Eclipse-MAT/"/>
    
      <category term="VisualVM" scheme="http://homoefficio.github.io/tags/VisualVM/"/>
    
      <category term="Heap Dump" scheme="http://homoefficio.github.io/tags/Heap-Dump/"/>
    
      <category term="Thread Dump" scheme="http://homoefficio.github.io/tags/Thread-Dump/"/>
    
      <category term="Memory Leak" scheme="http://homoefficio.github.io/tags/Memory-Leak/"/>
    
      <category term="jmap" scheme="http://homoefficio.github.io/tags/jmap/"/>
    
      <category term="jstat" scheme="http://homoefficio.github.io/tags/jstat/"/>
    
      <category term="Heap Shrinkage" scheme="http://homoefficio.github.io/tags/Heap-Shrinkage/"/>
    
  </entry>
  
  <entry>
    <title>Java Native Memory Tracking</title>
    <link href="http://homoefficio.github.io/2020/04/09/Java-Native-Memory-Tracking/"/>
    <id>http://homoefficio.github.io/2020/04/09/Java-Native-Memory-Tracking/</id>
    <published>2020-04-09T07:26:35.000Z</published>
    <updated>2022-03-18T16:07:46.396Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Java-Native-Memory-Tracking"><a href="#Java-Native-Memory-Tracking" class="headerlink" title="Java Native Memory Tracking"></a>Java Native Memory Tracking</h1><h2 id="DMA"><a href="#DMA" class="headerlink" title="DMA"></a>DMA</h2><p>자바에서도 <code>DirectBuffer</code>를 이용해서 JVM Heap이 아닌 Native 메모리를 사용하고 DMA(Direct Memory Access)의 장점을 활용할 수 있다.</p><p>구체적인 사용법 등 자세한 내용은 <a href="https://homoefficio.github.io/2019/02/27/Java-NIO-Direct-Buffer를-이용해서-대용량-파일-행-기준으로-쪼개기/">Java NIO Direct Buffer를 이용해서 대용량 파일 행 기준으로 쪼개기</a>를 참고하고 장단점만 요약하면 다음과 같다.</p><h3 id="장점"><a href="#장점" class="headerlink" title="장점"></a>장점</h3><ul><li>디스크에 있는 파일을 운영체제 메모리로 읽어들일 때 CPU를 건드리지 않는다.</li><li>운영체제 메모리에 있는 파일 내용을 JVM 내 메모리로 다시 복사할 필요가 없다.</li><li>JVM 내 힙 메모리를 쓰지 않으므로 GC를 유발하지 않는다.(물론 일정 크기를 가진 버퍼가 운영체제 메모리에 생성되는 것이고, 이 버퍼에 대한 참조 자체는 JVM 메모리 내에 생성된다)</li></ul><h3 id="단점"><a href="#단점" class="headerlink" title="단점"></a>단점</h3><ul><li>DMA에 사용할 버퍼 생성 시 시간이 더 소요될 수 있다.</li><li>바이트 단위로 데이터를 취급하므로, 데이터를 행 단위로 취급하기 불편하다.</li><li>일반적인 Java 메모리 분석 방법으로는 추적할 수 없다.</li></ul><p>요는 대용량 파일을 사용할 때 <code>DirectBuffer</code>를 사용하면 DMA의 장점을 누릴 수 있고 단점을 피할 수 있다.</p><h2 id="메모리-사용-추적"><a href="#메모리-사용-추적" class="headerlink" title="메모리 사용 추적"></a>메모리 사용 추적</h2><p>그런데 JVM Heap 메모리가 아닌 Native를 사용하므로 힙 덤프나 스레드 덤프 분석, <code>jstat</code> 등 일반적인 Java 메모리 분석 방법으로는 추적이 안 된다.</p><p>대용량 파일 사용 시 장점이 많다고 하니 아무래도 <code>DirectBuffer</code> 크기도 크게 잡을 수록 성능 상으로는 유리하겠지만, 그 큰 메모리가 어떻게 사용되고 회수되는지 확인이 안 된다면 곤란하다.</p><p>어쩌지?</p><p>뭘 어째.. 검색이지.. 검색해서 찾은 답은 <code>jcmd</code>다. </p><p>간략하게 알아보자.</p><h2 id="Java-실행-옵션-추가"><a href="#Java-실행-옵션-추가" class="headerlink" title="Java 실행 옵션 추가"></a>Java 실행 옵션 추가</h2><p>어떤 Java 애플리케이션의 Native 메모리 사용을 추적하려면 애플리케이션 실행 시 다음 옵션을 추가해줘야 한다.</p><blockquote><p>-XX:NativeMemoryTracking=summary</p></blockquote><h2 id="메모리-사용-현황-베이스라인-지정"><a href="#메모리-사용-현황-베이스라인-지정" class="headerlink" title="메모리 사용 현황 베이스라인 지정"></a>메모리 사용 현황 베이스라인 지정</h2><p>이제부터 알아볼 메모리 사용 추적 방법은 <strong>어떤 기준점 대비 메모리 사용량 증감(diff)을 기반으로 한다.</strong> 따라서 먼저 비교 기준이 될 베이스라인(기준점)을 지정해준다.</p><blockquote><p>jcmd {PID} VM.native_memory baseline</p></blockquote><p>위 명령을 실행하면 PID와 함께 <code>Baseline succeed</code>라는 짤막한 메시지만 출력된다. 비교 기준인 베이스라인이 지정됐다는 뜻이다.</p><h2 id="메모리-사용-현황-출력-초기"><a href="#메모리-사용-현황-출력-초기" class="headerlink" title="메모리 사용 현황 출력 - 초기"></a>메모리 사용 현황 출력 - 초기</h2><p>이제 다음 명령을 실행하면 <strong>메모리 사용 항목별로 베이스라인 대비 사용량 증감(diff)을 보여준다.</strong></p><blockquote><p>jcmd {PID} VM.native_memory summary.diff</p></blockquote><p>지금까지 수행한 베이스라인 지정과 초기 현황 출력 결과는 다음과 같다.</p><p><img src="https://i.imgur.com/SGbIKgm.png" alt="Imgur"></p><p>그리고 Native 메모리는 Internal 항목에 표시되며, 애플리케이션 실행 후 별다른 작업 없는 초기 상태에서 Native 메모리 사용량은 44KB 이다.</p><p>위 명령은 <code>jstat</code>처럼 주기적으로 실행하는 옵션은 없는 것 같고, 필요할 때마다 직접 실행하고 출력 내용을 확인하는 방식으로 진행한다.</p><h2 id="메모리-사용-현황-출력-DirectBuffer-사용-중"><a href="#메모리-사용-현황-출력-DirectBuffer-사용-중" class="headerlink" title="메모리 사용 현황 출력 - DirectBuffer 사용 중"></a>메모리 사용 현황 출력 - DirectBuffer 사용 중</h2><p><code>DirectBuffer</code>를 사용하는 작업을 실행한 후에 다시 <code>jcmd {PID} VM.native_memory summary.diff</code>를 실행하면 다음과 같이 Internal 항목의 사용량이 기존 44KB에서 150MB로 대폭 늘어난 것을 확인할 수 있다.</p><p><img src="https://i.imgur.com/t9gmDhx.png" alt="Imgur"></p><p>실제 코드에서도 다음과 같이 3개의 <code>DirectBuffer</code>를 각 50M 씩 할당했으므로 위 그림에서 출력된 내용과 잘 부합한다.</p><p><img src="https://i.imgur.com/2AfnkJj.png" alt="Imgur"></p><h2 id="메모리-사용-현황-출력-DirectBuffer-사용-완료-후"><a href="#메모리-사용-현황-출력-DirectBuffer-사용-완료-후" class="headerlink" title="메모리 사용 현황 출력 - DirectBuffer 사용 완료 후"></a>메모리 사용 현황 출력 - DirectBuffer 사용 완료 후</h2><p><code>DirectBuffer</code>를 사용하는 작업 완료 후 별다른 조치 없이 다시 <code>jcmd {PID} VM.native_memory summary.diff</code>를 실행하면 다음과 같이 Internal 항목의 사용량이 150MB에서 328KB로 대폭 줄어든 것을 확인할 수 있다.</p><p><img src="https://i.imgur.com/PRG5Nqh.png" alt="Imgur"></p><p>즉 <code>DirectBuffer</code> 사용이 끝난 후에 자동으로 Native 메모리가 정상적으로 회수됐음을 알 수 있다.</p><h2 id="더-읽을-거리"><a href="#더-읽을-거리" class="headerlink" title="더 읽을 거리"></a>더 읽을 거리</h2><ul><li>jcmd 명령 해설(공식 문서보다 훨씬 나음): <a href="https://www.javacodegeeks.com/2016/03/jcmd-one-jdk-command-line-tool-rule.html" target="_blank" rel="noopener">https://www.javacodegeeks.com/2016/03/jcmd-one-jdk-command-line-tool-rule.html</a></li></ul><h2 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h2><blockquote><ul><li><p>Java에서도 <code>DirectBuffer</code>를 이용해서 DMA를 활용할 수 있다.</p></li><li><p>DMA에 활용된 Native 메모리는 사용 완료 후 JVM GC와 무관하게(즉 다른 절차에 의해) 자동으로 반환된다.</p></li></ul></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Java-Native-Memory-Tracking&quot;&gt;&lt;a href=&quot;#Java-Native-Memory-Tracking&quot; class=&quot;headerlink&quot; title=&quot;Java Native Memory Tracking&quot;&gt;&lt;/a&gt;Java 
      
    
    </summary>
    
      <category term="Performance" scheme="http://homoefficio.github.io/categories/Performance/"/>
    
    
      <category term="Native Memory" scheme="http://homoefficio.github.io/tags/Native-Memory/"/>
    
      <category term="DirectBuffer" scheme="http://homoefficio.github.io/tags/DirectBuffer/"/>
    
      <category term="Direct Memory Access" scheme="http://homoefficio.github.io/tags/Direct-Memory-Access/"/>
    
      <category term="DMA" scheme="http://homoefficio.github.io/tags/DMA/"/>
    
      <category term="jcmd" scheme="http://homoefficio.github.io/tags/jcmd/"/>
    
      <category term="NativeMemoryTracking" scheme="http://homoefficio.github.io/tags/NativeMemoryTracking/"/>
    
      <category term="VM.native_memory" scheme="http://homoefficio.github.io/tags/VM-native-memory/"/>
    
      <category term="baseline" scheme="http://homoefficio.github.io/tags/baseline/"/>
    
      <category term="summary.diff" scheme="http://homoefficio.github.io/tags/summary-diff/"/>
    
  </entry>
  
  <entry>
    <title>Spring Data에서 Batch Insert 최적화</title>
    <link href="http://homoefficio.github.io/2020/01/25/Spring-Data%EC%97%90%EC%84%9C-Batch-Insert-%EC%B5%9C%EC%A0%81%ED%99%94/"/>
    <id>http://homoefficio.github.io/2020/01/25/Spring-Data에서-Batch-Insert-최적화/</id>
    <published>2020-01-25T14:06:24.000Z</published>
    <updated>2022-03-18T16:07:46.536Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Spring-Data에서-Batch-Insert-최적화"><a href="#Spring-Data에서-Batch-Insert-최적화" class="headerlink" title="Spring Data에서 Batch Insert 최적화"></a>Spring Data에서 Batch Insert 최적화</h1><p>Spring Data JPA가 안겨주는 편리함 뒤에는 가끔 성능 손실이 숨어있다. 이번에 알아볼 Batch Insert도 그런 예 중 하나다.</p><p>성능 손실 문제가 발생하는 이유와 2가지 해결 방법을 알아본다.</p><p>전체 코드는 <a href="https://github.com/HomoEfficio/micro-benchmark-spring-boot-batch-insert" target="_blank" rel="noopener">https://github.com/HomoEfficio/micro-benchmark-spring-boot-batch-insert</a> 여기에서 볼 수 있으며 아주 쉽게 직접 테스트해 볼 수도 있다.</p><h1 id="Batch-Insert란"><a href="#Batch-Insert란" class="headerlink" title="Batch Insert란?"></a>Batch Insert란?</h1><p>거창한 거 하나도 없다. 3건의 데이터를 insert 한다고 할 때,</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> table1 (col1, col2) <span class="keyword">VALUES</span> (val11, val12);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> table1 (col1, col2) <span class="keyword">VALUES</span> (val21, val22);</span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> table1 (col1, col2) <span class="keyword">VALUES</span> (val31, val32);</span><br></pre></td></tr></table></figure><p>이렇게 하면 개별 insert고,</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> table1 (col1, col2) <span class="keyword">VALUES</span></span><br><span class="line">(val11, val12),</span><br><span class="line">(val21, val22),</span><br><span class="line">(val31, val32);</span><br></pre></td></tr></table></figure><p>이렇게 하면 batch insert다. 그냥 봐도 batch insert 쪽이 훨씬 효율적임을 쉽게 알 수 있다.</p><p>DB 관점에서보면 간단한데, Spring Data에서 저런 DML이 DB로 전달되게 하는 건 그렇게 간단하지만은 않다.</p><h1 id="Hibernate의-Batch-Insert-제약-사항"><a href="#Hibernate의-Batch-Insert-제약-사항" class="headerlink" title="Hibernate의 Batch Insert 제약 사항"></a>Hibernate의 Batch Insert 제약 사항</h1><p><a href="https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#batch-session-batch-insert" target="_blank" rel="noopener">Hibernate 레퍼런스 문서 12.2.1. Batch inserts</a>의 바로 위에 다음과 같이 <strong>식별자 생성에 IDENTITY 방식을 사용하면 Hibernate가 JDBC 수준에서 batch insert를 비활성화</strong>한다고 나와있다.</p><blockquote><p>Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator.</p></blockquote><p>비활성화하는 이유는 Hibernate 문서에는 없는 것 같아서 다시 찾아보니 StackOverflow에 <a href="https://stackoverflow.com/a/27732138" target="_blank" rel="noopener">Vlad Mihalcea가 올린 댓글</a>에서 단서를 찾을 수 있었다.</p><blockquote><p>The only drawback is that we can’t know the newly assigned value prior to executing the INSERT statement. This restriction is hindering the “transactional write behind” flushing strategy adopted by Hibernate. For this reason, Hibernates disables the JDBC batch support for entities using the IDENTITY generator.</p></blockquote><p>요는 <strong>새로 할당할 Key 값을 미리 알 수 없는 IDENTITY 방식을 사용할 때 Batch Support를 지원하면 Hibernate가 채택한 flush 방식인 ‘Transactional Write Behind’와 충돌이 발생하기 때문</strong>에, IDENTITY 방식에서는 Batch Insert를 비활성화 한다는 얘기다. 따라서 그냥 <strong>일상적으로 가장 널리 사용하는 IDENTITY 방식을 사용하면 Batch Insert는 동작하지 않는다.</strong></p><p>그렇다고 Batch Insert를 적용하기 위해 IDENTITY 방식말고 섣불리 SEQUENCE 방식이나 TABLE 방식을 잘못 사용하면 더 나쁜 결과를 불러올 수 있다. <strong>채번에 따른 부하가 상당히 큰 SEQUENCE 방식이나 TABLE 방식을 별다른 조치 없이 사용하면 Batch Insert를 쓸 수 없는 IDENTITY 방식보다 더 느리다.</strong> 자세한 내용은 <a href="https://github.com/HomoEfficio/dev-tips/blob/master/JPA-GenerationType-별-INSERT-성능-비교.md" target="_blank" rel="noopener">https://github.com/HomoEfficio/dev-tips/blob/master/JPA-GenerationType-별-INSERT-성능-비교.md</a> 를 참고한다. 나름 건질만한 내용이 꽤 있으니 꼭 한 번 보길 권한다.</p><p>문제 발생 원인에서 유추할 수 있는 해결 방법은 2가지가 있다. </p><ol><li>SEQUENCE나 TABLE 방식을 사용하면서 채번 부하를 낮추는 방법</li><li>아예 Spring Data JPA를 벗어나는 방법</li></ol><h1 id="채번-부하-절감"><a href="#채번-부하-절감" class="headerlink" title="채번 부하 절감"></a>채번 부하 절감</h1><p>Batch Insert를 사용할 수 없는 IDENTITY 방식 대신에 SEQUENCE나 TABLE 방식을 사용하면서 채번 부하를 낮추는 방법은 <a href="https://dev.to/smartyansh/best-possible-hibernate-configuration-for-batch-inserts-2a7a" target="_blank" rel="noopener">https://dev.to/smartyansh/best-possible-hibernate-configuration-for-batch-inserts-2a7a</a> 에서 찾을 수 있었다.</p><p>간단하게 정리하면 채번 자체를 Batch 방식으로 처리해서 채번 부하를 낮추는 방식이다.</p><h2 id="일반적인-채번"><a href="#일반적인-채번" class="headerlink" title="일반적인 채번"></a>일반적인 채번</h2><p>SEQUENCE 방식은 일반적으로 다음과 같이 사용한다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Item</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="meta">@GeneratedValue</span>(strategy = GenerationType.SEQUENCE)</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>이 방식을 사용하면 Sequence를 지원하는 DB에서는 Sequence를 이용해서 채번한다. 아래는 Sequence를 지원하는 H2 DB를 사용했을 때 나오는 Hibernate 로그 일부다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">Hibernate: call next value for hibernate_sequence</span><br><span class="line">Hibernate: call next value for hibernate_sequence</span><br><span class="line">Hibernate: call next value for hibernate_sequence</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>Sequence를 지원하지 않는 DB에서는 Table을 이용해서 채번한다. 아래는 Sequence를 지원하지 않는 MySQL DB의 로그 일부다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">SET autocommit=0</span><br><span class="line">select next_val as id_val from hibernate_sequence for update</span><br><span class="line">update hibernate_sequence set next_val= 2 where next_val=1</span><br><span class="line">commit</span><br><span class="line">autocommit=1</span><br><span class="line">autocommit=0</span><br><span class="line">select next_val as id_val from hibernate_sequence for update</span><br><span class="line">update hibernate_sequence set next_val= 3 where next_val=2</span><br><span class="line">commit</span><br><span class="line">SET autocommit=1</span><br><span class="line">SET autocommit=0</span><br><span class="line">select next_val as id_val from hibernate_sequence for update</span><br><span class="line">update hibernate_sequence set next_val= 4 where next_val=3</span><br><span class="line">commit</span><br><span class="line">SET autocommit=1</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>번호 하나 딸 때마다 쿼리를 2개씩 날리게 되므로 꽤 큰 부하가 발생될 것임을 짐작할 수 있다.</p><h2 id="Batch-채번"><a href="#Batch-채번" class="headerlink" title="Batch 채번"></a>Batch 채번</h2><p>채번 자체를 Batch로 처리하면 아래와 같이 500개씩 한꺼번에 채번해서 쿼리 횟수를 대폭 줄이고 성능을 높일 수 있다.</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">SET autocommit=0</span><br><span class="line">select next_val as id_val from hibernate_sequence for update</span><br><span class="line">update hibernate_sequence set next_val= 501 where next_val=1</span><br><span class="line">commit</span><br><span class="line">SET autocommit=1</span><br><span class="line">SET autocommit=0</span><br><span class="line">select next_val as id_val from hibernate_sequence for update</span><br><span class="line">update hibernate_sequence set next_val= 1001 where next_val=501</span><br><span class="line">commit</span><br><span class="line">SET autocommit=1</span><br><span class="line">SET autocommit=0</span><br><span class="line">select next_val as id_val from hibernate_sequence for update</span><br><span class="line">update hibernate_sequence set next_val= 1501 where next_val=1001</span><br><span class="line">commit</span><br><span class="line">SET autocommit=1</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>Batch 채번은 다음과 같이 다소 복잡한 Hibernate 전용 애너테이션을 지정해야 한다. </p><p>채번 배치 크기도 애너테이션 내에서 지정해야 하므로 배치 크기 설정을 yml 파일로 외부화 할 수 없다는 단점이 있다. 또한 채번 배치 크기는 엔티티 클래스에서 Hibernate 애너테이션으로 지정해야 하고, Batch Insert의 배치 크기는 yml 파일로 지정하므로 두 값이 달라질 가능성이 있다는 것도 운영 상 단점이라고 할 수 있겠다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ItemSequence</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="meta">@GenericGenerator</span>(</span><br><span class="line">            name = <span class="string">"SequenceGenerator"</span>,</span><br><span class="line">            strategy = <span class="string">"org.hibernate.id.enhanced.SequenceStyleGenerator"</span>,</span><br><span class="line">            parameters = &#123;</span><br><span class="line">                    <span class="meta">@Parameter</span>(name = <span class="string">"sequence_name"</span>, value = <span class="string">"hibernate_sequence"</span>),</span><br><span class="line">                    <span class="meta">@Parameter</span>(name = <span class="string">"optimizer"</span>, value = <span class="string">"pooled"</span>),</span><br><span class="line">                    <span class="meta">@Parameter</span>(name = <span class="string">"initial_value"</span>, value = <span class="string">"1"</span>),</span><br><span class="line">                    <span class="meta">@Parameter</span>(name = <span class="string">"increment_size"</span>, value = <span class="string">"500"</span>)</span><br><span class="line">            &#125;</span><br><span class="line">    )</span><br><span class="line">    <span class="meta">@GeneratedValue</span>(</span><br><span class="line">            strategy = GenerationType.SEQUENCE,</span><br><span class="line">            generator = <span class="string">"SequenceGenerator"</span></span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line">    <span class="keyword">private</span> String description;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="Spring-Data-JDBC"><a href="#Spring-Data-JDBC" class="headerlink" title="Spring Data JDBC"></a>Spring Data JDBC</h1><p>Spring Data에는 JPA만 있는 것이 아니다. <a href="https://spring.io/projects/spring-data" target="_blank" rel="noopener">https://spring.io/projects/spring-data</a> 에 보면 상당히 다양한 저장소를 지원하는 서브 프로젝트가 많이 있으며, 지금처럼 관계형 데이터베이스에서는 JPA 대신 JDBC를 사용할 수도 있다.</p><h2 id="JdbcTemplate-batchUpdate"><a href="#JdbcTemplate-batchUpdate" class="headerlink" title="JdbcTemplate.batchUpdate()"></a>JdbcTemplate.batchUpdate()</h2><p>JdbcTemplate에는 Batch를 지원하는 <code>batchUpdate()</code> 메서드가 마련돼있다. 여러 가지로 Overloading 돼 있어서 편리한 메서드를 골라서 사용하면 되는데, 여기에서는 batch 크기를 지정할 수 있는 <code>BatchPreparedStatementSetter</code>를 사용하는 아래의 메서드를 사용해서 구현해본다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">batchUpdate(String sql, BatchPreparedStatementSetter pss)</span><br></pre></td></tr></table></figure><h2 id="주요-구현-부분"><a href="#주요-구현-부분" class="headerlink" title="주요 구현 부분"></a>주요 구현 부분</h2><p><code>ItemJdbc</code>라는 객체를 <code>ITEM_JDBC</code> 테이블에 Batch Insert로 저장한다고 가정하고, 주요 구현부를 살펴보면 다음과 같다. </p><p><code>batchSize</code> 변수를 통해 배치 크기를 지정하고, 전체 데이터를 배치 크기로 나눠서 Batch Insert를 실행하고, 자투리 데이터를 다시 Batch Insert로 저장한다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Repository</span></span><br><span class="line"><span class="meta">@RequiredArgsConstructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ItemJdbcRepositoryImpl</span> <span class="keyword">implements</span> <span class="title">ItemJdbcRepository</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> JdbcTemplate jdbcTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value</span>(<span class="string">"$&#123;batchSize&#125;"</span>)</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> batchSize;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">saveAll</span><span class="params">(List&lt;ItemJdbc&gt; items)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">int</span> batchCount = <span class="number">0</span>;</span><br><span class="line">        List&lt;ItemJdbc&gt; subItems = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; items.size(); i++) &#123;</span><br><span class="line">            subItems.add(items.get(i));</span><br><span class="line">            <span class="keyword">if</span> ((i + <span class="number">1</span>) % batchSize == <span class="number">0</span>) &#123;</span><br><span class="line">                batchCount = batchInsert(batchSize, batchCount, subItems);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (!subItems.isEmpty()) &#123;</span><br><span class="line">            batchCount = batchInsert(batchSize, batchCount, subItems);</span><br><span class="line">        &#125;</span><br><span class="line">        System.out.println(<span class="string">"batchCount: "</span> + batchCount);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">batchInsert</span><span class="params">(<span class="keyword">int</span> batchSize, <span class="keyword">int</span> batchCount, List&lt;ItemJdbc&gt; subItems)</span> </span>&#123;</span><br><span class="line">        jdbcTemplate.batchUpdate(<span class="string">"INSERT INTO ITEM_JDBC (`NAME`, `DESCRIPTION`) VALUES (?, ?)"</span>,</span><br><span class="line">                <span class="keyword">new</span> BatchPreparedStatementSetter() &#123;</span><br><span class="line">                    <span class="meta">@Override</span></span><br><span class="line">                    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setValues</span><span class="params">(PreparedStatement ps, <span class="keyword">int</span> i)</span> <span class="keyword">throws</span> SQLException </span>&#123;</span><br><span class="line">                        ps.setString(<span class="number">1</span>, subItems.get(i).getName());</span><br><span class="line">                        ps.setString(<span class="number">2</span>, subItems.get(i).getDescription());</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="meta">@Override</span></span><br><span class="line">                    <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getBatchSize</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                        <span class="keyword">return</span> subItems.size();</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;);</span><br><span class="line">        subItems.clear();</span><br><span class="line">        batchCount++;</span><br><span class="line">        <span class="keyword">return</span> batchCount;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="실험-결과"><a href="#실험-결과" class="headerlink" title="실험 결과"></a>실험 결과</h1><p>어느 방식이 가장 빠를까?</p><p>아래와 같은 환경에서 테스트 해 본 결과 <strong>Spring Data JDBC 방식이 가장 빠르다.</strong></p><h2 id="테스트-환경"><a href="#테스트-환경" class="headerlink" title="테스트 환경"></a>테스트 환경</h2><ul><li>Java 11</li><li>Spring Boot 2.2.4</li><li>MySQL 5.7.18</li><li>Sprint Data JPA 2.2.4</li><li>Hibernate Core 5.4.10.Final</li><li>Hibernate Commons Annotations 5.1.0.Final</li><li>Sprint Data JDBC 1.1.4</li></ul><h2 id="성능-비교"><a href="#성능-비교" class="headerlink" title="성능 비교"></a>성능 비교</h2><p>그렇다면 얼마나 차이가 날까?</p><p>연관 관계 없이 단 하나의 엔티티만 저장하는 시나리오에서, 배치 크기를 바꿔가면서 10,000건의 데이터를 저장하는 실험 결과 소요 시간(초 단위) 및 비교 배율은 다음과 같다.</p><table><thead><tr><th>배치 크기</th><th>JDBC(A)</th><th>Batch SEQUENCE(B)</th><th>IDENTITY(C)</th><th>(B)/(A)</th><th>(C)/(A)</th></tr></thead><tbody><tr><td>10</td><td>0.885</td><td>3.072</td><td>5.087</td><td>3.47</td><td>5.748022599</td></tr><tr><td>50</td><td>0.391</td><td>1.007</td><td>4.097</td><td>2.58</td><td>10.47826087</td></tr><tr><td>100</td><td>0.356</td><td>0.808</td><td>5.218</td><td>2.27</td><td>14.65730337</td></tr><tr><td>500</td><td>0.226</td><td>0.515</td><td>5.637</td><td>2.28</td><td>24.94247788</td></tr><tr><td>1000</td><td>0.216</td><td>0.480</td><td>6.241</td><td>2.22</td><td>28.89351852</td></tr><tr><td>5000</td><td>0.189</td><td>0.447</td><td>5.052</td><td>2.37</td><td>26.73015873</td></tr></tbody></table><p>배치 크기에 따라 다르지만, <strong>Spring Data JDBC의 <code>batchUpdate()</code>를 사용하는 방식이 Hibernate Batch Sequence 방식보다 대략 2 ~ 3배 정도 빠르고, Batch Insert가 사용되지 못 하는 Hibernate IDENTITY 방식보다는 5 ~ 25배 정도 빠르다.</strong> </p><p>MySQL에는 Sequence가 없으므로 SEQUENCE 방식을 지정했다고 하더라도 사실 상 TABLE 방식으로 동작했다는 것을 감안하면, <strong>Sequence가 지원되는 DB에서는 TABLE 방식보다 채번 부하가 더 적은 SEQUENCE 방식을 Batch 스타일로 사용하면 Spring Data JDBC 방식과 비슷한 성능을 보일 것 같다.</strong></p><h1 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h1><blockquote><ul><li><p><strong>아주 많은 수의 데이터를 한 꺼번에 입력할 때는 Spring Data JPA를 잠시 뒤로하고 Spring Data JDBC의 <code>batchUpdate()</code>를 활용하는 것도 좋다.</strong></p><ul><li>Spring Data JDBC는 Spring Data JPA와 함께 혼용해서 사용할 수도 있고,</li><li><code>@Transactional</code>을 통해 트랜잭션이 관리될 수 있으므로,</li><li>현실적으로 가장 나은 방법이다.</li></ul></li><li><p><strong>Spring Data JPA를 사용해야만 한다면 IDENTITY 방식 말고 Batch SEQUENCE 방식을 사용하는 것이 좋다.</strong></p><ul><li>그러나 이 방식은 애너테이션 지정이 필요 이상 복잡하고,</li><li>테이블 생성 시부터 적용하면 괜찮지만, 이미 ID 생성 방식이 IDENTITY인 기존 테이블을 SEQUENCE 방식으로 변경해야 하는 부담이 있고,</li><li>batch 크기 지정 관련 운영 상의 단점이 있다.</li></ul></li></ul></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Spring-Data에서-Batch-Insert-최적화&quot;&gt;&lt;a href=&quot;#Spring-Data에서-Batch-Insert-최적화&quot; class=&quot;headerlink&quot; title=&quot;Spring Data에서 Batch Insert 최적화&quot;&gt;
      
    
    </summary>
    
      <category term="Performance" scheme="http://homoefficio.github.io/categories/Performance/"/>
    
    
      <category term="Performance" scheme="http://homoefficio.github.io/tags/Performance/"/>
    
      <category term="Spring Data" scheme="http://homoefficio.github.io/tags/Spring-Data/"/>
    
      <category term="Spring Data JDBC" scheme="http://homoefficio.github.io/tags/Spring-Data-JDBC/"/>
    
      <category term="Spring Data JPA" scheme="http://homoefficio.github.io/tags/Spring-Data-JPA/"/>
    
      <category term="Batch Insert" scheme="http://homoefficio.github.io/tags/Batch-Insert/"/>
    
      <category term="Bulk Insert" scheme="http://homoefficio.github.io/tags/Bulk-Insert/"/>
    
      <category term="MySQL" scheme="http://homoefficio.github.io/tags/MySQL/"/>
    
      <category term="Hibernate" scheme="http://homoefficio.github.io/tags/Hibernate/"/>
    
      <category term="GenerationType" scheme="http://homoefficio.github.io/tags/GenerationType/"/>
    
      <category term="GenerationType.IDENTITY" scheme="http://homoefficio.github.io/tags/GenerationType-IDENTITY/"/>
    
      <category term="GenerationType.SEQUENCE" scheme="http://homoefficio.github.io/tags/GenerationType-SEQUENCE/"/>
    
      <category term="GenerationType.TABLE" scheme="http://homoefficio.github.io/tags/GenerationType-TABLE/"/>
    
      <category term="GenerationType.AUTO" scheme="http://homoefficio.github.io/tags/GenerationType-AUTO/"/>
    
      <category term="IDENTITY column" scheme="http://homoefficio.github.io/tags/IDENTITY-column/"/>
    
      <category term="Auto Increment" scheme="http://homoefficio.github.io/tags/Auto-Increment/"/>
    
      <category term="Sequence" scheme="http://homoefficio.github.io/tags/Sequence/"/>
    
      <category term="Database" scheme="http://homoefficio.github.io/tags/Database/"/>
    
  </entry>
  
  <entry>
    <title>GET이냐 POST냐 그것이 문제로다</title>
    <link href="http://homoefficio.github.io/2019/12/25/GET%EC%9D%B4%EB%83%90-POST%EB%83%90-%EA%B7%B8%EA%B2%83%EC%9D%B4-%EB%AC%B8%EC%A0%9C%EB%A1%9C%EB%8B%A4/"/>
    <id>http://homoefficio.github.io/2019/12/25/GET이냐-POST냐-그것이-문제로다/</id>
    <published>2019-12-25T03:04:39.000Z</published>
    <updated>2022-03-18T16:07:46.246Z</updated>
    
    <content type="html"><![CDATA[<h1 id="GET이냐-POST냐-그것이-문제로다"><a href="#GET이냐-POST냐-그것이-문제로다" class="headerlink" title="GET이냐 POST냐 그것이 문제로다"></a>GET이냐 POST냐 그것이 문제로다</h1><p>며칠 전에 <a href="https://www.facebook.com/hanmomhanda/posts/10221495156952479" target="_blank" rel="noopener">페이스북에 올렸던 질문</a>에 여러분께서 시간 내서 좋은 의견 나눠주셔서, 나만 꿀꺽하고 넘어가면 도리가 아닌 것 같아 다시 정리해본다.</p><p>먼저 이 글은 <strong>나름의 결론이 있기는 하지만, 그것이 정답이라고 단정할 수는 없다.</strong><br>또한 REST와는 아무런 관계가 없으며, 오직 HTTP Method에 대한 얘기다.</p><h1 id="문제"><a href="#문제" class="headerlink" title="문제"></a>문제</h1><p>보통 클라이언트 쪽에서 Contents를 제공하면서 새로운 Resource의 생성을 요청할 때는, 그 Contents를 포함시켜서 POST로 요청을 보내면 된다. 여기에는 별다른 이견이 없다.</p><p>그런데 클라이언트 쪽에서 아무런 Contents를 제공하지 않으면서, 그저 서버로부터 어떤 Resource를 반환받으려 하는데, 실제로 서버에서는 새 Resource를 생성해서 반환해야 하는 경우라면, GET을 써야 하나 아니면 POST를 써야 하나?</p><p>예를 들어 클라이언트가 아무 내용 없이 그냥 ‘퀴즈를 내다오’라고 서버에게 요청하면,<br>즉, 반환되는 퀴즈가 새로 만든 건지 기존에 만들어져 있던 걸 반환하는지 클라이언트는 관심 가질 필요가 없는 상황이라면,</p><ul><li>GET …/quizzes/new 로 보낸다. (GET이지만 서버는 알아서 새 퀴즈를 생성해서 반환)</li><li>POST (내용없이) …/quizzes 로 보낸다. (POST지만 서버는 새로 생성된 퀴즈를 반환)</li></ul><p>GET, POST 둘 중 어느 쪽이 더 환영받는 방법인가? 또는 어느 쪽이 욕을 덜 먹는 방법인가? 또는 다른 더 나은 방안이 있다면 어떤게 있을까?</p><p>의견을 모아서 정리해봤다. 거의 원문에 가깝고 괄호 안은 임의로 추가.</p><h1 id="POST라는-의견"><a href="#POST라는-의견" class="headerlink" title="POST라는 의견"></a>POST라는 의견</h1><blockquote><p>멱등성이 유지될 수 있으면 GET, 없으면 POST 라는 기본 전제를 두고 생각합니다. 그래서 (새 퀴즈가 생성된다면 멱등이 아니므로) POST로 할 것 같습니다.</p></blockquote><blockquote><p>명시적으로 URI는 자원에 대한 것이어야 하는데 없는 자원을 내놓으라면 404를 반환하는 게 맞을 것 같습니다. (새 자원을 만들어 반환해야 하므로 POST로 할 것 같습니다.)</p></blockquote><blockquote><p>디비 상태에 변화를 주는 건 GET을 쓰지 않고 있어요. GET을 사용할 때는 순수함수처럼 같은 인풋은 같은 아웃풋을 내줘야 한다고 생각합니다.</p></blockquote><blockquote><p>http 1.0이후 생겨난 POST, PUT, DELETE…는 서버 내에 있는 자원에 관련된 조작이기 때문에 전 DB의 변화가 있으면 무조건 상태 변경과 관련된 메서드를 사용합니다.(리소스란 측면에서 DB는 일부…) http 프로토콜에서 method 영역을 클라이언트 입장이냐 서버 입장이냐로 다들 관점이 다르게 바라볼수 있겠지만 http 역사를 생각해보면 메서드는 자원과 관련이 크고 자원을 소유한 서버측 관점에 무게를 싣고 있습니다.</p></blockquote><blockquote><p>POST로 하고 생성된 자원에 대한 GET 경로를 Location 헤더에 넣어서 201로 응답하는 게 맞을 것 같습니다.</p></blockquote><h1 id="GET이라는-의견"><a href="#GET이라는-의견" class="headerlink" title="GET이라는 의견"></a>GET이라는 의견</h1><blockquote><p>클라이언트는 퀴즈를 원할 뿐 새로 생성되든 기존에 있던 거든 신경 쓸필요가 없다고 하니, 조회의 의미만 존재한다고 생각해요.</p></blockquote><blockquote><p>클라이언트는 이게 새로 만들어진건지 기존에 있던건지 모르지만 일단 원하는 행위가 퀴즈라는 것을 얻기 위함이기에 GET 이 맞는거 같습니다.<br>빵집을 예로 들어 빵을 산다라는 행위에서 이게 기존에 만들어진것을 사가든 주문과 동시에 만들어진 빵을 사가든 같은 행위지만(GET)<br>이런 빵을 사가려는데 만들어주세요 혹은 이런빵을 만들어주시면 사러 가겠습니다 (POST)는 다른 목적이고 빵집에서도 이에 따라 다른 행동을 취해야 하니까요</p></blockquote><h1 id="둘-다-아니라는-의견"><a href="#둘-다-아니라는-의견" class="headerlink" title="둘 다 아니라는 의견"></a>둘 다 아니라는 의견</h1><blockquote><p>API는 행위 중심이어야지 상태 중심이면 안 된다는 것이 OOP와 DDD를 통한 배움이었습니다.</p></blockquote><h1 id="중간-정리"><a href="#중간-정리" class="headerlink" title="중간 정리"></a>중간 정리</h1><p>이 말도 맞는 것 같고 저 말도 맞는 것 같고, 멱등성(idempotence)이라는 어려운 용어도 나오고 아 현기증나..</p><p>하지만 정신차리고 추려보면 결국 아래와 같이 요약할 수 있다.</p><blockquote><ul><li>요청자인 클라이언트의 의도를 중요하게 보는 입장에서는 GET을 선호  </li><li>HTTP는 결국 자원을 다루는 것이므로 자원을 중요하게 보는 입장에서는 POST를 선호</li></ul></blockquote><h1 id="스펙은-뭐라더냐"><a href="#스펙은-뭐라더냐" class="headerlink" title="스펙은 뭐라더냐?"></a>스펙은 뭐라더냐?</h1><p>이쯤되면 그다지 보고 싶지 않은 스펙을 보지 않을 수 없다. 관련 스펙은 <a href="https://tools.ietf.org/html/rfc7231" target="_blank" rel="noopener">Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content</a>이며 그 중에서 HTTP Method 관련 내용은 <a href="https://tools.ietf.org/html/rfc7231#section-4" target="_blank" rel="noopener">여기</a>에 있다.</p><p>POST를 지지하는 의견은 한 마디로 요약하면 다음과 같다.</p><blockquote><p>자원 변경이 수반되면 POST여야 한다.</p></blockquote><p>그런데 정말 스펙에서도 ‘자원 변경이 수반되면 POST여야 한다’고 규정하고 있을까?</p><h2 id="Safe-Methods"><a href="#Safe-Methods" class="headerlink" title="Safe Methods"></a>Safe Methods</h2><p>스펙 내용 중에 <a href="https://tools.ietf.org/html/rfc7231#section-4.2.1" target="_blank" rel="noopener">Safe Methods</a> 라는 단원이 있다.</p><blockquote><p>Request methods are considered “safe” if their defined semantics are<br>essentially read-only; i.e., the client does not request, and does<br>not expect, any state change on the origin server as a result of<br>applying a safe method to a target resource.  Likewise, reasonable<br>use of a safe method is not expected to cause any harm, loss of<br>property, or unusual burden on the origin server.</p></blockquote><p>짧게 옮겨 보면 다음과 같다.</p><blockquote><p>클라이언트가 서버 상태의 변경을 요청하지도, 기대하지도 않는 읽기 전용 요청은 Safe하다고 볼 수 있다. 그래서 Safe Method를 바르게 사용하면 서버에게 어떤 해악이나 손실, 일반적이지 않은 부담을 발생시키지 않는다.</p></blockquote><p>여기까지만 보면 자원 변경이 수반되면 GET을 쓰면 안 될 것 같다.</p><p>그런데 바로 다음 문단에는 살짝 결이 다른 내용이 나온다.</p><blockquote><p>This definition of safe methods does not prevent an implementation<br>from including behavior that is potentially harmful, that is not<br>entirely read-only, or that causes side effects while invoking a safe<br>method.  What is important, however, is that the client did not<br>request that additional behavior and cannot be held accountable for<br>it.  For example, most servers append request information to access<br>log files at the completion of every response, regardless of the<br>method, and that is considered safe even though the log storage might<br>become full and crash the server.  Likewise, a safe request initiated<br>by selecting an advertisement on the Web will often have the side<br>effect of charging an advertising account.</p></blockquote><p>역시나 짧게 옮겨 보면,</p><blockquote><p><strong>Safe Method라고해서 사이드 이펙트나 잠재적으로 해가 될 수 있는 동작을 포함해서 구현하는 것을 배제하지는 않는다. 중요한 것은 그 동작을 클라이언트가 요청한 게 아니라는 점이고, 그 동작에 대한 책임을 클라이언트가 부담하지 않는다는 점이다.</strong> 예를 들어 서버는 (Safe든 아니든) 메서드 종류에 관계 없이 모든 요청에 대해 액세스 로그를 기록하는데, 액세스 로그로 하드가 꽉 차서 서버가 깨질 수도 있지만, (로그 기록은 클라이언트가 요청한 것이 아니므로) 이런 호출도 Safe하다고 본다.(이하 광고 사례 생략)</p></blockquote><p>이 외에도 GET, POST를 직접적으로 설명하는 부분도 있지만, 이 글 내용에 크게 영향을 미치는 내용은 없어 보여서 굳이 다루지 않는다.</p><h1 id="그래서-결론은"><a href="#그래서-결론은" class="headerlink" title="그래서 결론은?"></a>그래서 결론은?</h1><p>앞에서도 말했지만 이 글은 나름의 주관적인 결론은 있지만 그게 정답은 아니다.</p><p>이미 꽤 길어졌으니 결론부터 말하면 <strong>GET을 써도 좋겠다</strong>이다.</p><p>이유는,</p><ul><li>API라는 게 결국 쌍방간의 계약이고,  </li><li>클라이언트는 본질적으로 어떤 자원을 얻기를 바랄 뿐 굳이 자원 생성 여부를 알 필요가 없다면,  </li><li>즉, 클라이언트의 본질적인 요구가 ‘생성’이 아니라 ‘획득’이라면,  </li><li>클라이언트의 요청 처리 내부 과정에 ‘생성’이라는 비멱등 과정이 포함된다고 하더라도,  </li><li>서버의 처리 과정보다는 클라이언트의 ‘획득’이라는 요구 본질에 무게를 두어도,</li><li>스펙에 어긋남이 없기 때문이다.</li></ul><p>게다가 다음 같은 상황을 가정해보면 GET을 써도 좋겠다 정도가 아니라 <strong>GET이 더 낫다</strong>라는 생각도 든다.</p><p>퀴즈를 처음에는 클라이언트 요청에 그때그때 생성해서 반환하기로 하고 이건 자원 생성을 유발하니 POST로 하자.. 로 시작했는데,<br>나중에 퀴즈 서비스가 흥해서 클라이언트가 엄청 많아지고 성능이든 뭐든 여타 이유로 ‘가만 퀴즈를 꼭 생성해서 반환할 필요 없지 않아? 미리 왕창 만들어 놓고 임의로 걍 조회만 해서 반환하는 게 나을 것 같은데?’라는 판단이 든다. 그럼 이제 자원 생성이 발생하지 않으므로 GET을 써야 한다.</p><p>클라이언트의 요구는 ‘퀴즈의 획득’으로 변한 게 없는데, 서버의 처리 과정이 신규 자원 생성에서 기존 자원 조회로 바뀌었다고 해서 API를 POST에서 GET으로 바꿔야되나? 수많은 클라이언트에게 GET으로 바꿔달라고 모두 설득할 수 있나?</p><p>애초에 자원 생성과 무관하게 오로지 ‘획득’이었던 클라이언트의 요청 본질에 충실하게 GET으로 시작했다면 이런 큰 변경을 피할 수 있었을 것이다.</p><p>이렇게 보면 <strong>HTTP Method의 사용에서도 ‘비멱등이면 POST’와 같은 원칙보다는, Information Hiding(정보 감춤/숨김) 같은 더 일반적인 상위 차원의 설계 원칙이 유연한 시스템을 구축하는 데 더 중요</strong>한 것 같다.</p><h1 id="추가"><a href="#추가" class="headerlink" title="추가"></a>추가</h1><p>공유하고 나니 의견을 조금 더 받을 수 있었다.</p><blockquote><p>본 문제에서는 User Interface 와 실제 Http Client 가 분리되어 판단해야 한다고 생각합니다. User Interface 에서는 당연히 사람은 문제를 받는 것만 생각할 것이고 내부 Http Client 는 문제 생성을 중간에 넣어줘도 된다고 생각하는 것입니다.(그래서 내부 Http Client는 POST로 요청해서 문제를 생성하도록 하고 다시 GET으로 요청해서 생성된 문제를 받아오라는 의견)</p></blockquote><blockquote><p>혹시 POST로 생성하게 하면 id를 반환하고 그 id로 데이터를 받아오게 API를 나누면 어떨까요?</p></blockquote><p>둘 다 비슷한 의견인데, <strong>클라이언트가 문제 생성에 관심이 없더라도, 문제가 새로 생성되는 것이 맞다면 (UI수준에서는 인지 못 하게 하더라도 내부적으로) POST로 문제 생성 요청 후, 생성된 문제를 GET으로 가져오자는 의견</strong>으로 보인다.</p><p>위 퀴즈 사례에 국한해서라면 여전히 GET이 더 낫다고 생각한다. 이유는 <strong>위 퀴즈 사례는 처음에는 퀴즈 생성으로 시작하지만 나중에 조회 방식으로 변경될 개연성이 꽤 있고, 자원 보다는 클라이언트의 의도에 무게를 두는 GET이 변경 대응에 더 유연하기 때문</strong>이다.</p><p>다만 위 추가 의견을 옮겨온 이유는, 위 의견 덕분에 앞서 GET이 낫다는 결론이 <strong>이와 비슷해 보이는 상황에서 (언제나) GET을 쓰는 게 낫다라는 잘못된 가이드가 될 수 있는 위험</strong>이 있음을 깨닫게 됐기 떄문이다.</p><p>퀴즈 사례와는 달리 <strong>나중에 조회 방식으로 변경될 개연성이 매우 낮은 상황이라면, 위 의견과 같이 POST + GET 방식을 사용하는 게 합당할 수도 있다.</strong> 다만 이런 결론마저도, 한 번의 HTTP 요청이라도 줄여야 하는 상황이라면 GET을 선택하는 편이 낫게 된다. </p><p>결국 글 서두에서 밝힌 대로 이 글 자체로 정답은 될 수 없다. 그저 참고가 될 뿐이고 <strong>주어진 상황에 맞는 결론을 도출하는 것은 언제나 엔지니어의 몫이다.</strong></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;GET이냐-POST냐-그것이-문제로다&quot;&gt;&lt;a href=&quot;#GET이냐-POST냐-그것이-문제로다&quot; class=&quot;headerlink&quot; title=&quot;GET이냐 POST냐 그것이 문제로다&quot;&gt;&lt;/a&gt;GET이냐 POST냐 그것이 문제로다&lt;/h1&gt;&lt;
      
    
    </summary>
    
      <category term="Concepts" scheme="http://homoefficio.github.io/categories/Concepts/"/>
    
    
      <category term="HTTP" scheme="http://homoefficio.github.io/tags/HTTP/"/>
    
      <category term="HTTP API" scheme="http://homoefficio.github.io/tags/HTTP-API/"/>
    
      <category term="HTTP Method" scheme="http://homoefficio.github.io/tags/HTTP-Method/"/>
    
      <category term="Safe Method" scheme="http://homoefficio.github.io/tags/Safe-Method/"/>
    
      <category term="Idempotence" scheme="http://homoefficio.github.io/tags/Idempotence/"/>
    
      <category term="GET" scheme="http://homoefficio.github.io/tags/GET/"/>
    
      <category term="POST" scheme="http://homoefficio.github.io/tags/POST/"/>
    
      <category term="Information Hiding" scheme="http://homoefficio.github.io/tags/Information-Hiding/"/>
    
      <category term="정보 숨김" scheme="http://homoefficio.github.io/tags/%EC%A0%95%EB%B3%B4-%EC%88%A8%EA%B9%80/"/>
    
      <category term="정보 감춤" scheme="http://homoefficio.github.io/tags/%EC%A0%95%EB%B3%B4-%EA%B0%90%EC%B6%A4/"/>
    
      <category term="멱등" scheme="http://homoefficio.github.io/tags/%EB%A9%B1%EB%93%B1/"/>
    
  </entry>
  
  <entry>
    <title>Raspberry Pi 3에 Ubuntu 설치 하기</title>
    <link href="http://homoefficio.github.io/2019/12/21/Raspberry-Pi-3%EC%97%90-Ubuntu-%EC%84%A4%EC%B9%98-%ED%95%98%EA%B8%B0/"/>
    <id>http://homoefficio.github.io/2019/12/21/Raspberry-Pi-3에-Ubuntu-설치-하기/</id>
    <published>2019-12-21T08:34:41.000Z</published>
    <updated>2022-03-18T16:07:46.478Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Install-Ubuntu-on-Raspberry-Pi-3"><a href="#Install-Ubuntu-on-Raspberry-Pi-3" class="headerlink" title="Install Ubuntu on Raspberry Pi 3"></a>Install Ubuntu on Raspberry Pi 3</h1><h2 id="준비물"><a href="#준비물" class="headerlink" title="준비물"></a>준비물</h2><ul><li>Raspberry Pi 3 + 전원 장치</li><li>MicroSD 카드</li><li>USB Keyboard</li><li>Monitor + HDMI 케이블</li><li>Ubuntu 이미지를 다운로드 하고 MicroSD에 Ubuntu 이미지를 Flash 할 인터넷 연결 컴퓨터</li></ul><h2 id="Ubuntu-이미지-파일-다운로드"><a href="#Ubuntu-이미지-파일-다운로드" class="headerlink" title="Ubuntu 이미지 파일 다운로드"></a>Ubuntu 이미지 파일 다운로드</h2><ul><li><a href="https://ubuntu.com/download/raspberry-pi" target="_blank" rel="noopener">https://ubuntu.com/download/raspberry-pi</a><ul><li><code>64-bit for Raspberry Pi 3 and 4</code> 다운로드</li></ul></li></ul><p>약 3G 정도로 다운로드에 시간이 좀 걸리므로 그동안 아래의 ‘Image Flash 프로그램 다운로드 및 설치’, ‘MicroSD 메모리 카드 준비’ 수행</p><h2 id="Image-Flash-프로그램-다운로드-및-설치"><a href="#Image-Flash-프로그램-다운로드-및-설치" class="headerlink" title="Image Flash 프로그램 다운로드 및 설치"></a>Image Flash 프로그램 다운로드 및 설치</h2><ul><li><a href="https://sourceforge.net/projects/win32diskimager/files/latest/download" target="_blank" rel="noopener">https://sourceforge.net/projects/win32diskimager/files/latest/download</a></li></ul><h2 id="MicroSD-메모리-카드-준비"><a href="#MicroSD-메모리-카드-준비" class="headerlink" title="MicroSD 메모리 카드 준비"></a>MicroSD 메모리 카드 준비</h2><p>윈도우 10 기준</p><ul><li><p>MicroSD 메모리 카드를 컴퓨터에 연결</p><ul><li>포맷 등의 팝업이 뜨면 모두 무시</li><li><img src="https://i.imgur.com/93adzTR.png" alt="Imgur"></li><li><img src="https://i.imgur.com/UM67S8u.png" alt="Imgur"></li></ul></li><li><p>기존에 사용하던 카드라 파티션이 나뉘어 있는 경우 파티션 삭제</p><ul><li>윈도우 버튼 우클릭 &gt; 디스크 관리자 실행</li><li><img src="https://i.imgur.com/cGeAsxt.png" alt="Imgur"></li><li>MicroSD 메모리에 있던 볼륨(파티션) 모두 삭제</li><li><img src="https://i.imgur.com/hr9w14r.png" alt="Imgur"></li><li>아래와 같이 모두 삭제 되면 준비 완료</li><li><img src="https://i.imgur.com/rPKVxcJ.png" alt="Imgur"></li></ul></li></ul><h2 id="MicroSD-메모리-카드에-Ubuntu-이미지-파일-Flash"><a href="#MicroSD-메모리-카드에-Ubuntu-이미지-파일-Flash" class="headerlink" title="MicroSD 메모리 카드에 Ubuntu 이미지 파일 Flash"></a>MicroSD 메모리 카드에 Ubuntu 이미지 파일 Flash</h2><ul><li>Ubuntu 이미지 파일 압축 해제<ul><li>필요 시 반디집 설치 후 해제</li></ul></li><li>Image Flash 프로그램 실행<ul><li>Ubuntu 이미지 파일 위치 지정 및 Flash 할 대상 디바이스(MicroSD 카드) 지정 후 Write</li><li><img src="https://i.imgur.com/hFTpk31.png" alt="Imgur"></li><li><img src="https://i.imgur.com/p2inqJO.png" alt="Imgur"></li></ul></li><li>사양에 따라 다르겠지만 약 2~3분 후 다음과 같이 Flash 완료<ul><li><img src="https://i.imgur.com/00yqpDc.png" alt="Imgur"></li></ul></li><li>포맷 팝업창이 다시 뜨면 무시<ul><li><img src="https://i.imgur.com/uV31oML.png" alt="Imgur"></li></ul></li><li>탐색기에서 꺼내기 후 MicroSD 메모리 카드를 빼낸다.</li></ul><h2 id="Raspberry-Pi-부팅-설치-완료-및-로그인"><a href="#Raspberry-Pi-부팅-설치-완료-및-로그인" class="headerlink" title="Raspberry Pi 부팅, 설치 완료 및 로그인"></a>Raspberry Pi 부팅, 설치 완료 및 로그인</h2><ul><li>Ubuntu 이미지가 Flash 된 MicroSD 카드, 모니터와 연결된 HDMI 케이블과 키보드를 Raspberry PI 에 연결하고 마지막으로 Raspberry PI 전원 연결<ul><li><img src="https://i.imgur.com/YwBAux3.jpg" alt="Imgur"></li></ul></li><li>Ubuntu 로 부팅되며 몇 분간 자동 설정 후 로그인 프롬프트 나옴<ul><li><img src="https://i.imgur.com/5P5wgZ6.jpg" alt="Imgur"></li><li><img src="https://i.imgur.com/9G6Zqos.jpg" alt="Imgur"></li><li><img src="https://i.imgur.com/JkXmeLR.jpg" alt="Imgur"></li><li><img src="https://i.imgur.com/LV5d4or.jpg" alt="Imgur"></li><li><strong>놀랍게도 위 사진에 나오는 로그인 프롬프트는 페이크..</strong> 여기서 입력해봤자 비번 틀리다는 얘기만 나오며, 그냥 기다리면 다음 사진과 같이 후속 절차가 자동으로 계속 진행된다.</li><li><img src="https://i.imgur.com/dgSIjFH.jpg" alt="Imgur"></li><li><img src="https://i.imgur.com/dggNhk9.jpg" alt="Imgur"></li></ul></li><li>다음과 같이 <strong><code>[  OK  ] Reached target Cloud-init target.</code> 이 보여야 로그인 준비가 완료</strong>된 것이다. 하지만 <strong>자동으로 로그인 프롬프트가 뜨지는 않고 엔터를 눌러줘야 로그인 프롬프트가 뜬다.</strong><ul><li><img src="https://i.imgur.com/oL5bc8f.jpg" alt="Imgur"></li></ul></li><li>초기 아이디/비번은 ubuntu/ubuntu 이며 로그인 후 위 그림과 같이 비번 변경하면 셸 프롬프트가 뜬다.<ul><li><img src="https://i.imgur.com/njltjMN.jpg" alt="Imgur"></li></ul></li><li>이것으로 부팅, 설치, 로그인 완료</li><li>다음 명령으로 리눅스를 종료한다.<ul><li><code>shutdown -h now</code></li></ul></li></ul><h2 id="설치-완료-후-부팅-및-로그인"><a href="#설치-완료-후-부팅-및-로그인" class="headerlink" title="설치 완료 후 부팅 및 로그인"></a>설치 완료 후 부팅 및 로그인</h2><ul><li>전원을 연결하면 산딸기 그림과 함께 부팅 과정이 진행되고 다음과 같이.. 이번에도 페이크성 로그인 프롬프트가 나온다..</li><li>좀더 기다리면 나머지 후속 작업이 진행되고 <code>Up ##.## seconds</code> 라고 표시되는데 이제서야 비로소 부팅 과정이 완료된 것이다. 하지만 이번에도 진행이 완료된 건지 화면만으로는 알 길이 없다.. </li><li>엔터를 누르면 다음과 같이 진정 유효한 로그인 프롬프트가 나오며, 로그인을 하면 셸 프롬프트가 표시된다.<ul><li><img src="https://i.imgur.com/3iU0f8k.jpg" alt="Imgur"></li></ul></li></ul><h2 id="설치-후기"><a href="#설치-후기" class="headerlink" title="설치 후기"></a>설치 후기</h2><ul><li>사실 그냥 <a href="https://ubuntu.com/download/raspberry-pi" target="_blank" rel="noopener">https://ubuntu.com/download/raspberry-pi</a> 여기에 나온 공식 설명을 재연하고 몇 가지 실제 발생하는 상황(MicroSD 초기화)을 추가한 것 뿐이지만..</li><li>위에 나오는 것처럼 어떤 단계가 언제 끝난 건지 잘 알기 어려운 장면들이 있고,</li><li>페이크성 로그인 프롬프트 처럼 살짝 버그스럽게 보이는 지뢰들이 몇 군데 있음을 감안하면,</li><li>남기길 잘했다..</li></ul><h2 id="고정-IP-적용"><a href="#고정-IP-적용" class="headerlink" title="고정 IP 적용"></a>고정 IP 적용</h2><p>IP 주소는 <code>/etc/netplan/50-cloud-init.yaml</code> 파일에서 설정할 수 있으며 기본은 다음과 같이 설정돼있다.</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># This file is generated from information provided by</span></span><br><span class="line"><span class="comment"># the datasource.  Changes to it will not persist across an instance.</span></span><br><span class="line"><span class="comment"># To disable cloud-init's network configuration capabilities, write a file</span></span><br><span class="line"><span class="comment"># /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:</span></span><br><span class="line"><span class="comment"># network: &#123;config: disabled&#125;</span></span><br><span class="line"><span class="attr">network:</span></span><br><span class="line"><span class="attr">    ethernets:</span></span><br><span class="line"><span class="attr">        eth0:</span>            </span><br><span class="line"><span class="attr">            dhcp4:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">            optional:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">    version:</span> <span class="number">2</span></span><br></pre></td></tr></table></figure><h3 id="설정-파일-수정"><a href="#설정-파일-수정" class="headerlink" title="설정 파일 수정"></a>설정 파일 수정</h3><p>고정 IP 주소를 적용하려면 <code>sudo vi</code>로 다음과 같이 편집한다.</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># This file is generated from information provided by</span></span><br><span class="line"><span class="comment"># the datasource.  Changes to it will not persist across an instance.</span></span><br><span class="line"><span class="comment"># To disable cloud-init's network configuration capabilities, write a file</span></span><br><span class="line"><span class="comment"># /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:</span></span><br><span class="line"><span class="comment"># network: &#123;config: disabled&#125;</span></span><br><span class="line"><span class="attr">network:</span></span><br><span class="line"><span class="attr">    ethernets:</span></span><br><span class="line"><span class="attr">        eth0:</span></span><br><span class="line"><span class="attr">            addresses:</span> <span class="string">[사용할.고정.IP.주소/24]</span></span><br><span class="line"><span class="attr">            gateway4:</span> <span class="string">사용할.내부.게이트웨이.주소</span></span><br><span class="line"><span class="attr">            nameservers:</span></span><br><span class="line"><span class="attr">                addresses:</span> <span class="string">[168.126.63.1,</span> <span class="number">8.8</span><span class="number">.8</span><span class="number">.8</span><span class="string">]</span></span><br><span class="line"><span class="attr">            dhcp4:</span> <span class="literal">no</span></span><br><span class="line"><span class="attr">            optional:</span> <span class="literal">no</span></span><br><span class="line"><span class="attr">    version:</span> <span class="number">2</span></span><br></pre></td></tr></table></figure><h3 id="설정-내용-적용"><a href="#설정-내용-적용" class="headerlink" title="설정 내용 적용"></a>설정 내용 적용</h3><p>다음 명령으로 설정 내용을 적용한다.</p><blockquote><p>sudo netplan apply</p></blockquote><h3 id="설정-내용-확인"><a href="#설정-내용-확인" class="headerlink" title="설정 내용 확인"></a>설정 내용 확인</h3><p>다음 명령으로 IP 주소를 확인한다.</p><blockquote><p>ip addr</p></blockquote>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Install-Ubuntu-on-Raspberry-Pi-3&quot;&gt;&lt;a href=&quot;#Install-Ubuntu-on-Raspberry-Pi-3&quot; class=&quot;headerlink&quot; title=&quot;Install Ubuntu on Raspberry 
      
    
    </summary>
    
      <category term="개발 환경 및 도구" scheme="http://homoefficio.github.io/categories/%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD-%EB%B0%8F-%EB%8F%84%EA%B5%AC/"/>
    
    
      <category term="Raspberry Pi" scheme="http://homoefficio.github.io/tags/Raspberry-Pi/"/>
    
      <category term="Ubuntu" scheme="http://homoefficio.github.io/tags/Ubuntu/"/>
    
      <category term="IoT" scheme="http://homoefficio.github.io/tags/IoT/"/>
    
      <category term="라즈베리 파이" scheme="http://homoefficio.github.io/tags/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC-%ED%8C%8C%EC%9D%B4/"/>
    
      <category term="우분투" scheme="http://homoefficio.github.io/tags/%EC%9A%B0%EB%B6%84%ED%88%AC/"/>
    
      <category term="IP" scheme="http://homoefficio.github.io/tags/IP/"/>
    
      <category term="Static IP" scheme="http://homoefficio.github.io/tags/Static-IP/"/>
    
  </entry>
  
</feed>
